Fix hacktoberfest applications (#15613)
As title, make them syncable with twenty-cli:0.2.0 <img width="1025" height="693" alt="image" src="https://github.com/user-attachments/assets/a18f8ba3-b6fc-40e9-84dd-446ff8deeb04" />
This commit is contained in:
-24
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "c3ec36c8-5b1d-421f-9172-a9e035ab9c18",
|
||||
"name": "calculaterollups",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "eec8aaf2-b0cc-47fd-b522-8d4aa5fe4bd3",
|
||||
"type": "databaseEvent",
|
||||
"eventName": "opportunity.*"
|
||||
},
|
||||
{
|
||||
"universalIdentifier": "a3fea230-1121-44a6-b395-5811c3031f8e",
|
||||
"type": "cron",
|
||||
"schedule": " 0 2 * * *"
|
||||
},
|
||||
{
|
||||
"universalIdentifier": "d33b0fe4-4b2b-45c0-aa2f-e617fdbba484",
|
||||
"type": "route",
|
||||
"path": "/recalculate-all",
|
||||
"httpMethod": "POST",
|
||||
"isAuthRequired": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+37
-22
@@ -10,7 +10,10 @@ export const computeAggregations = (
|
||||
now: Date,
|
||||
) => {
|
||||
const baseFiltered = applyFilters(records, definition.childFilters, now);
|
||||
const result: Record<string, number | string | null | Record<string, unknown>> = {};
|
||||
const result: Record<
|
||||
string,
|
||||
number | string | null | Record<string, unknown>
|
||||
> = {};
|
||||
|
||||
definition.aggregations.forEach((aggregation) => {
|
||||
const scopedRecords = applyFilters(baseFiltered, aggregation.filters, now);
|
||||
@@ -40,7 +43,7 @@ export const computeAggregations = (
|
||||
return accumulator;
|
||||
}
|
||||
return {
|
||||
amount: accumulator.amount + numeric,
|
||||
amount: (accumulator.amount as number) + numeric,
|
||||
currency:
|
||||
typeof currencyRaw === 'string' && currencyRaw.trim().length > 0
|
||||
? currencyRaw
|
||||
@@ -50,7 +53,7 @@ export const computeAggregations = (
|
||||
{ amount: 0, currency: undefined as string | undefined },
|
||||
);
|
||||
result[aggregation.parentField] = {
|
||||
amountMicros: Math.round(roundForSum(total.amount)),
|
||||
amountMicros: Math.round(roundForSum(total.amount as number)),
|
||||
currencyCode: total.currency ?? '',
|
||||
};
|
||||
break;
|
||||
@@ -72,46 +75,58 @@ export const computeAggregations = (
|
||||
return accumulator;
|
||||
}
|
||||
return {
|
||||
total: accumulator.total + numeric,
|
||||
count: accumulator.count + 1,
|
||||
total: (accumulator.total as number) + numeric,
|
||||
count: (accumulator.count as number) + 1,
|
||||
};
|
||||
},
|
||||
{ total: 0, count: 0 },
|
||||
);
|
||||
result[aggregation.parentField] = count === 0 ? null : roundForSum(total / count);
|
||||
result[aggregation.parentField] =
|
||||
count === 0
|
||||
? null
|
||||
: roundForSum((total as number) / (count as number));
|
||||
break;
|
||||
}
|
||||
case 'MAX':
|
||||
case 'MIN': {
|
||||
if (!aggregation.childField) {
|
||||
throw new Error(`${aggregation.type} aggregation requires childField`);
|
||||
const childField = aggregation.childField;
|
||||
if (!childField) {
|
||||
throw new Error(
|
||||
`${aggregation.type} aggregation requires childField`,
|
||||
);
|
||||
}
|
||||
|
||||
const direction = aggregation.type === 'MAX' ? 1 : -1;
|
||||
let chosen: { raw: unknown; comparable: number | null } | null = null;
|
||||
scopedRecords.forEach((record) => {
|
||||
const rawValue = getNestedValue(record, aggregation.childField!);
|
||||
let chosen: { raw: unknown; comparable: number } | null = null;
|
||||
|
||||
for (const record of scopedRecords) {
|
||||
const rawValue = getNestedValue(record, childField);
|
||||
const comparable = toComparableNumber(rawValue);
|
||||
if (comparable === null) {
|
||||
return;
|
||||
}
|
||||
if (comparable === null) continue;
|
||||
|
||||
if (
|
||||
chosen === null ||
|
||||
(chosen.comparable !== null && direction * (comparable - chosen.comparable) > 0)
|
||||
direction * (comparable - chosen.comparable) > 0
|
||||
) {
|
||||
chosen = { raw: rawValue, comparable };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (chosen === null) {
|
||||
result[aggregation.parentField] = null;
|
||||
} else if (typeof chosen.raw === 'number') {
|
||||
result[aggregation.parentField] = chosen.raw;
|
||||
} else if (chosen.raw instanceof Date) {
|
||||
result[aggregation.parentField] = chosen.raw.toISOString().slice(0, 10);
|
||||
} else if (chosen.raw === null || chosen.raw === undefined) {
|
||||
break;
|
||||
}
|
||||
|
||||
const raw = chosen.raw;
|
||||
|
||||
if (typeof raw === 'number') {
|
||||
result[aggregation.parentField] = raw;
|
||||
} else if (raw instanceof Date) {
|
||||
result[aggregation.parentField] = raw.toISOString().slice(0, 10);
|
||||
} else if (raw == null) {
|
||||
result[aggregation.parentField] = null;
|
||||
} else {
|
||||
const rawString = String(chosen.raw);
|
||||
const rawString = String(raw);
|
||||
const parsed = Date.parse(rawString);
|
||||
result[aggregation.parentField] = Number.isNaN(parsed)
|
||||
? rawString
|
||||
|
||||
+45
-14
@@ -28,18 +28,27 @@ const aggregationTypes = new Set<AggregationConfig['type']>([
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null;
|
||||
|
||||
export const validateRollupConfig = (config: unknown): asserts config is RollupConfig => {
|
||||
export const validateRollupConfig = (config: any): RollupConfig | void => {
|
||||
if (!Array.isArray(config)) {
|
||||
throw new Error('Rollup configuration must contain an array of rollup definitions');
|
||||
throw new Error(
|
||||
'Rollup configuration must contain an array of rollup definitions',
|
||||
);
|
||||
}
|
||||
|
||||
config.forEach((definition, definitionIndex) => {
|
||||
if (!isObject(definition)) {
|
||||
throw new Error(`Definition at index ${definitionIndex} must be an object`);
|
||||
throw new Error(
|
||||
`Definition at index ${definitionIndex} must be an object`,
|
||||
);
|
||||
}
|
||||
|
||||
const { parentObject, childObject, relationField, childFilters, aggregations } =
|
||||
definition as Partial<RollupDefinition>;
|
||||
const {
|
||||
parentObject,
|
||||
childObject,
|
||||
relationField,
|
||||
childFilters,
|
||||
aggregations,
|
||||
} = definition as Partial<RollupDefinition>;
|
||||
|
||||
if (typeof parentObject !== 'string' || parentObject.trim().length === 0) {
|
||||
throw new Error(`Definition ${definitionIndex} missing parentObject`);
|
||||
@@ -47,11 +56,16 @@ export const validateRollupConfig = (config: unknown): asserts config is RollupC
|
||||
if (typeof childObject !== 'string' || childObject.trim().length === 0) {
|
||||
throw new Error(`Definition ${definitionIndex} missing childObject`);
|
||||
}
|
||||
if (typeof relationField !== 'string' || relationField.trim().length === 0) {
|
||||
if (
|
||||
typeof relationField !== 'string' ||
|
||||
relationField.trim().length === 0
|
||||
) {
|
||||
throw new Error(`Definition ${definitionIndex} missing relationField`);
|
||||
}
|
||||
if (!Array.isArray(aggregations) || aggregations.length === 0) {
|
||||
throw new Error(`Definition ${definitionIndex} must declare at least one aggregation`);
|
||||
throw new Error(
|
||||
`Definition ${definitionIndex} must declare at least one aggregation`,
|
||||
);
|
||||
}
|
||||
|
||||
const filtersToValidate = [
|
||||
@@ -63,7 +77,8 @@ export const validateRollupConfig = (config: unknown): asserts config is RollupC
|
||||
);
|
||||
}
|
||||
|
||||
const { type, parentField, childField, filters } = aggregation as AggregationConfig;
|
||||
const { type, parentField, childField, filters } =
|
||||
aggregation as AggregationConfig;
|
||||
|
||||
if (!aggregationTypes.has(type)) {
|
||||
throw new Error(
|
||||
@@ -71,13 +86,19 @@ export const validateRollupConfig = (config: unknown): asserts config is RollupC
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof parentField !== 'string' || parentField.trim().length === 0) {
|
||||
if (
|
||||
typeof parentField !== 'string' ||
|
||||
parentField.trim().length === 0
|
||||
) {
|
||||
throw new Error(
|
||||
`Aggregation ${aggregationIndex} in definition ${definitionIndex} missing parentField`,
|
||||
);
|
||||
}
|
||||
|
||||
if (type !== 'COUNT' && (typeof childField !== 'string' || childField.length === 0)) {
|
||||
if (
|
||||
type !== 'COUNT' &&
|
||||
(typeof childField !== 'string' || childField.length === 0)
|
||||
) {
|
||||
throw new Error(
|
||||
`Aggregation ${aggregationIndex} in definition ${definitionIndex} with type ${type} requires childField`,
|
||||
);
|
||||
@@ -132,7 +153,11 @@ const collectValuesByKey = (
|
||||
}
|
||||
|
||||
Object.entries(value).forEach(([currentKey, entryValue]) => {
|
||||
if (currentKey === key && typeof entryValue === 'string' && entryValue.trim().length > 0) {
|
||||
if (
|
||||
currentKey === key &&
|
||||
typeof entryValue === 'string' &&
|
||||
entryValue.trim().length > 0
|
||||
) {
|
||||
result.add(entryValue);
|
||||
return;
|
||||
}
|
||||
@@ -160,11 +185,17 @@ export const resolveRollupConfig = (): RollupConfig => {
|
||||
try {
|
||||
const parsed = JSON.parse(override) as unknown;
|
||||
validateRollupConfig(parsed);
|
||||
return parsed;
|
||||
return parsed as RollupConfig;
|
||||
} catch (error) {
|
||||
const reason =
|
||||
error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
|
||||
throw new Error(`Unable to parse rollup configuration override (reason: ${reason})`);
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === 'string'
|
||||
? error
|
||||
: 'Unknown error';
|
||||
throw new Error(
|
||||
`Unable to parse rollup configuration override (reason: ${reason})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+61
-11
@@ -2,7 +2,8 @@ import { computeAggregations } from './aggregations';
|
||||
import { buildChildRecordIndex, TwentyClient } from './client';
|
||||
import { extractRelationValues, resolveRollupConfig } from './config';
|
||||
import { getNestedValue } from './filtering';
|
||||
import type { ExecutionSummaryItem, RollupDefinition } from './types';
|
||||
import type { ExecutionSummaryItem } from './types';
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null;
|
||||
@@ -60,17 +61,26 @@ const getApiCredentials = () => {
|
||||
return { apiKey, baseUrl };
|
||||
};
|
||||
|
||||
const formatSummary = (summaries: ExecutionSummaryItem[], durationMs: number) => ({
|
||||
const formatSummary = (
|
||||
summaries: ExecutionSummaryItem[],
|
||||
durationMs: number,
|
||||
) => ({
|
||||
status: 'ok',
|
||||
tookMs: durationMs,
|
||||
totals: {
|
||||
processed: summaries.reduce((accumulator, item) => accumulator + item.processed, 0),
|
||||
updated: summaries.reduce((accumulator, item) => accumulator + item.updated, 0),
|
||||
processed: summaries.reduce(
|
||||
(accumulator, item) => accumulator + item.processed,
|
||||
0,
|
||||
),
|
||||
updated: summaries.reduce(
|
||||
(accumulator, item) => accumulator + item.updated,
|
||||
0,
|
||||
),
|
||||
},
|
||||
details: summaries,
|
||||
});
|
||||
|
||||
export async function main(params: unknown): Promise<object> {
|
||||
export const main = async (params: unknown): Promise<any> => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const config = resolveRollupConfig();
|
||||
@@ -92,7 +102,9 @@ export async function main(params: unknown): Promise<object> {
|
||||
|
||||
const credentials = getApiCredentials();
|
||||
if (!credentials) {
|
||||
console.warn('[rollup] skipping execution because TWENTY_API_KEY is not set');
|
||||
console.warn(
|
||||
'[rollup] skipping execution because TWENTY_API_KEY is not set',
|
||||
);
|
||||
return { status: 'noop', reason: 'TWENTY_API_KEY not configured' };
|
||||
}
|
||||
|
||||
@@ -102,7 +114,9 @@ export async function main(params: unknown): Promise<object> {
|
||||
const summaries: ExecutionSummaryItem[] = [];
|
||||
|
||||
for (const definition of config) {
|
||||
const targetIds = fullRebuild ? undefined : relationCache.get(definition.relationField);
|
||||
const targetIds = fullRebuild
|
||||
? undefined
|
||||
: relationCache.get(definition.relationField);
|
||||
|
||||
if (!fullRebuild && (!targetIds || targetIds.size === 0)) {
|
||||
summaries.push({
|
||||
@@ -116,7 +130,11 @@ export async function main(params: unknown): Promise<object> {
|
||||
continue;
|
||||
}
|
||||
|
||||
const childIndex = await buildChildRecordIndex(definition, client, targetIds);
|
||||
const childIndex = await buildChildRecordIndex(
|
||||
definition,
|
||||
client,
|
||||
targetIds,
|
||||
);
|
||||
|
||||
const updates: Array<{
|
||||
id: string;
|
||||
@@ -138,13 +156,21 @@ export async function main(params: unknown): Promise<object> {
|
||||
console.info(
|
||||
`[rollup] computed aggregates for ${definition.parentObject} ${parentId}: ${JSON.stringify(payload)}`,
|
||||
);
|
||||
updates.push({ id: parentId, payload, context: { relationId: parentId } });
|
||||
updates.push({
|
||||
id: parentId,
|
||||
payload,
|
||||
context: { relationId: parentId },
|
||||
});
|
||||
});
|
||||
|
||||
let updatedCount = 0;
|
||||
for (const update of updates) {
|
||||
try {
|
||||
await client.updateObject(definition.parentObject, update.id, update.payload);
|
||||
await client.updateObject(
|
||||
definition.parentObject,
|
||||
update.id,
|
||||
update.payload,
|
||||
);
|
||||
updatedCount += 1;
|
||||
console.info(
|
||||
`[rollup] updated ${definition.parentObject} ${update.id} (relation ${update.context.relationId})`,
|
||||
@@ -204,4 +230,28 @@ export async function main(params: unknown): Promise<object> {
|
||||
: 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: 'c3ec36c8-5b1d-421f-9172-a9e035ab9c18',
|
||||
name: 'calculaterollups',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: 'eec8aaf2-b0cc-47fd-b522-8d4aa5fe4bd3',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'opportunity.*',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a3fea230-1121-44a6-b395-5811c3031f8e',
|
||||
type: 'cron',
|
||||
pattern: ' 0 2 * * *',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'd33b0fe4-4b2b-45c0-aa2f-e617fdbba484',
|
||||
type: 'route',
|
||||
path: '/recalculate-all',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user