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:
martmull
2025-11-04 18:40:06 +01:00
committed by GitHub
parent b5064e88f6
commit 86a6e04d78
86 changed files with 2329 additions and 981 deletions
@@ -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
}
]
}
@@ -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
@@ -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})`,
);
}
}
@@ -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,
},
],
};