Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore: upgrade cloud function v1 to v2 #27

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/configStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const defaultConfig = {
region: 'us-central1',
validatePrivilege: defaultValidatePrivilege,
validateVisibility: defaultValidateVisibility,
unwrapResponse: defaultUnwrapResponse,
unwrapResponse: defaultUnwrapResponse,
middleware: [],
corsEnabled: true,
corsOptions: defaultCorsOptions,
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const applyModifiers = require('./validateFields/applyModifiers')
const iFC = require('./iFC.js');
const publish = require('./pubSub/publish');

module.exports = require('./v2');

module.exports.default = createFunctions;
module.exports.createFunctions = createFunctions;
module.exports.generateTypes = generateTypes;
Expand Down
160 changes: 160 additions & 0 deletions src/v2/createFunctionsV2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
const { onMessagePublished } = require('firebase-functions/v2/pubsub');
const { onSchedule } = require('firebase-functions/v2/scheduler');
const { onRequest } = require('firebase-functions/v2/https');
const { onDocumentDeleted, onDocumentUpdated, onDocumentCreated } = require('firebase-functions/v2/firestore');
const parseRoutes = require('../parseRoutes');
const withIdempotencyV2 = require('../ensureIdempotent');
const keepFunctionAlive = require('../keepFunctionAlive');
const configStore = require('../configStore');
const publishChangesV2 = require('./publishChangesV2');
const parseMessageV2 = require('./pubSub/parseMessageV2');
const ignoreOldEventsV2 = require('./ignoreOldEventsV2');

const fromEntries = [(acc, [key, value]) => ({ ...acc, [key]: value }), {}];

const flattenObjects = [(acc, el) => ({ ...acc, ...el }), {}];

const setupRoutesV2 = (config, service) =>
Array.isArray(service.routes)
? {
[service.basePath]: onRequest(
{
region: config.region,
...(config.runtimeOptions || service.runtimeOptions),
},
parseRoutes(config, service)
),
}
: {};

const setupOnCreateV2 = (config, service) =>
onDocumentCreated(
{
region: config.region,
document: service.resourcePath,
},
publishChangesV2(service.basePath)
);

const setupOnUpdateV2 = (config, service) =>
onDocumentUpdated(
{
region: config.region,
document: service.resourcePath,
},
publishChangesV2(service.basePath)
);

const setupOnDeleteV2 = (config, service) =>
onDocumentDeleted(
{
region: config.region,
document: service.resourcePath,
},
publishChangesV2(service.basePath)
);

const setupDBTriggersV2 = (config, service) =>
service.publishChanges
? {
[`${service.basePath}_onCreate`]: setupOnCreateV2(config, service),
[`${service.basePath}_onUpdate`]: setupOnUpdateV2(config, service),
[`${service.basePath}_onDelete`]: setupOnDeleteV2(config, service),
}
: {};

const setupKeepAliveV2 = (config, service) =>
service.keepAlive
? {
[`${service.basePath}_keep_alive`]: onSchedule(
{ region: config.region, schedule: "every 5 minutes" },
keepFunctionAlive(service)
),
}
: {};

const setupScheduleV2 =
(config, service) =>
({
time,
name,
function: toExecute,
timeZone = "America/New_York",
runtimeOptions,
}) =>
[
[`${service.basePath}_${name}`],
onSchedule(
{
schedule: time,
region: config.region,
timeZone,
...(runtimeOptions || service.runtimeOptions),
},
toExecute
),
];

const setupSchedulesV2 = (config, service) =>
Array.isArray(service.schedule)
? service.schedule
.map(setupScheduleV2(config, service))
.reduce(...fromEntries)
: {};

const setupEventV2 =
(config, service) =>
({
topic,
type = "",
function: toExecute,
ensureIdempotent = false,
maxAge,
runtimeOptions,
}) => {
const functionName = `${service.basePath}_${topic}${type ? `_${type}` : ""
}`;

const handlerWithIdempotency = ensureIdempotent
? withIdempotencyV2(functionName, toExecute)
: toExecute;

const handler = maxAge
? ignoreOldEventsV2(maxAge, handlerWithIdempotency)
: handlerWithIdempotency;

return [
functionName,
onMessagePublished(
{
topic,
region: config.region,
...(runtimeOptions || service.runtimeOptions),
},
parseMessageV2(handler)
),
];
};

const setupEventsV2 = (config, service) =>
Array.isArray(service.events)
? service.events.map(setupEventV2(config, service)).reduce(...fromEntries)
: {};

const parseConfigV2 = (config, service) => ({
...setupRoutesV2(config, service),
...setupSchedulesV2(config, service),
...setupDBTriggersV2(config, service),
...setupEventsV2(config, service),
...setupKeepAliveV2(config, service),
});

const createFunctionsV2 = (config = {}, services) => {
configStore.config = { ...configStore.config, ...config };

return services
.map((service) => parseConfigV2(configStore.config, service))
.reduce(...flattenObjects);
};

module.exports = createFunctionsV2;
25 changes: 25 additions & 0 deletions src/v2/ignoreOldEventsV2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* If an event continues to retry for a long period of time
* there may be a reason to just drop it and stop retrying
* This util, when applied, will bail out of a handler if
* it is greater than the maxAge set.
*/

function setupIgnoreOldEvents(maxAge, handler) {
return function ignoreOldEvents(...args) {
const [message, context] = args;
const {time} = message
const eventAgeMs = Date.now() - Date.parse(time);

if (eventAgeMs > maxAge) {
console.log(
`Dropping event ${context.eventId} with age[ms]: ${eventAgeMs}`
);
return true;
}

return handler(...args);
};
}

module.exports = setupIgnoreOldEvents;
6 changes: 6 additions & 0 deletions src/v2/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const createFunctionsV2 = require('./createFunctionsV2');
const parseMessageV2 = require('./pubSub/parseMessageV2');

module.exports.default = createFunctionsV2;
module.exports.createFunctionsV2 = createFunctionsV2;
module.exports.parseMessageV2 = parseMessageV2;
18 changes: 18 additions & 0 deletions src/v2/pubSub/parseMessageV2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const parseMessageV2 = callback => (event) => {
const { data } = event;
const {message} = data;

const newMessage = message.data
? Buffer.from(message.data, 'base64').toString()
: null;

if (!newMessage) {
throw new Error('message cannot be empty');
}

const parsedMessage = JSON.parse(newMessage);

return callback(parsedMessage, event);
};

module.exports = parseMessageV2;
29 changes: 29 additions & 0 deletions src/v2/publishChangesV2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const { PubSub } = require('@google-cloud/pubsub');

const pubsub = new PubSub();

module.exports = topic => async (event) => {
const { type, data } = event;
const childType = type.split('google.firebase.database.').pop();

let eventData;

if (childType === 'ref.v1.created') {
eventData = data.data();
} else if (childType === 'ref.v1.updated') {
eventData = data.after.data();
} else if (childType === 'ref.v1.deleted') {
eventData = data.data();
}

const newData = JSON.stringify({
type: childType,
data: eventData,
...(type === 'ref.v1.updated' ? { dataBefore: data.before.data() } : {}),
changeContext: event,
});

const dataBuffer = Buffer.from(newData);

return pubsub.topic(topic).publish(dataBuffer);
};