@lobb-js/core 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/package.json +48 -0
  2. package/src/Lobb.ts +150 -0
  3. package/src/LobbError.ts +105 -0
  4. package/src/TypesGenerator.ts +11 -0
  5. package/src/api/WebServer.ts +126 -0
  6. package/src/api/collections/CollectionControllers.ts +485 -0
  7. package/src/api/collections/CollectionService.ts +162 -0
  8. package/src/api/collections/collectionRoutes.ts +105 -0
  9. package/src/api/collections/collectionStore.ts +647 -0
  10. package/src/api/collections/transactions.ts +166 -0
  11. package/src/api/collections/utils.ts +73 -0
  12. package/src/api/errorHandler.ts +73 -0
  13. package/src/api/events/index.ts +129 -0
  14. package/src/api/meta/route.ts +66 -0
  15. package/src/api/meta/service.ts +163 -0
  16. package/src/api/middlewares.ts +71 -0
  17. package/src/api/openApiRoute.ts +1017 -0
  18. package/src/api/schema/SchemaService.ts +71 -0
  19. package/src/api/schema/schemaRoutes.ts +13 -0
  20. package/src/config/ConfigManager.ts +252 -0
  21. package/src/config/validations.ts +49 -0
  22. package/src/coreCollections/collectionsCollection.ts +56 -0
  23. package/src/coreCollections/index.ts +14 -0
  24. package/src/coreCollections/migrationsCollection.ts +36 -0
  25. package/src/coreCollections/queryCollection.ts +26 -0
  26. package/src/coreCollections/workflowsCollection.ts +73 -0
  27. package/src/coreDbSetup/index.ts +72 -0
  28. package/src/coreMigrations/index.ts +3 -0
  29. package/src/database/DatabaseService.ts +44 -0
  30. package/src/database/DatabaseSyncManager.ts +173 -0
  31. package/src/database/MigrationsManager.ts +95 -0
  32. package/src/database/drivers/MongoDriver.ts +750 -0
  33. package/src/database/drivers/pgDriver/PGDriver.ts +655 -0
  34. package/src/database/drivers/pgDriver/QueryBuilder.ts +474 -0
  35. package/src/database/drivers/pgDriver/utils.ts +6 -0
  36. package/src/events/EventSystem.ts +191 -0
  37. package/src/events/coreEvents/index.ts +218 -0
  38. package/src/events/studioEvents/index.ts +32 -0
  39. package/src/extension/ExtensionSystem.ts +236 -0
  40. package/src/extension/dashboardRoute.ts +35 -0
  41. package/src/fields/ArrayField.ts +33 -0
  42. package/src/fields/BoolField.ts +34 -0
  43. package/src/fields/DateField.ts +13 -0
  44. package/src/fields/DateTimeField.ts +13 -0
  45. package/src/fields/DecimalField.ts +13 -0
  46. package/src/fields/FieldUtils.ts +56 -0
  47. package/src/fields/FloatField.ts +13 -0
  48. package/src/fields/IntegerField.ts +13 -0
  49. package/src/fields/LongField.ts +13 -0
  50. package/src/fields/ObjectField.ts +15 -0
  51. package/src/fields/StringField.ts +13 -0
  52. package/src/fields/TextField.ts +13 -0
  53. package/src/fields/TimeField.ts +13 -0
  54. package/src/index.ts +53 -0
  55. package/src/studio/Studio.ts +108 -0
  56. package/src/types/CollectionControllers.ts +15 -0
  57. package/src/types/DatabaseDriver.ts +115 -0
  58. package/src/types/Extension.ts +46 -0
  59. package/src/types/Field.ts +29 -0
  60. package/src/types/apiSchema.ts +12 -0
  61. package/src/types/collectionServiceSchema.ts +18 -0
  62. package/src/types/config/collectionFields.ts +85 -0
  63. package/src/types/config/collectionsConfig.ts +50 -0
  64. package/src/types/config/config.ts +66 -0
  65. package/src/types/config/relations.ts +17 -0
  66. package/src/types/filterSchema.ts +88 -0
  67. package/src/types/index.ts +38 -0
  68. package/src/types/migrations.ts +12 -0
  69. package/src/types/websockets.ts +34 -0
  70. package/src/types/workflows/processors.ts +1 -0
  71. package/src/utils/lockCollectionToObject.ts +204 -0
  72. package/src/utils/utils.ts +310 -0
  73. package/src/workflows/WorkflowSystem.ts +182 -0
  74. package/src/workflows/coreWorkflows/collectionsTable/index.ts +118 -0
  75. package/src/workflows/coreWorkflows/index.ts +18 -0
  76. package/src/workflows/coreWorkflows/processors/postOperationsWorkflows.ts +46 -0
  77. package/src/workflows/coreWorkflows/processors/preOperationsWorkflows.ts +27 -0
  78. package/src/workflows/coreWorkflows/processors/processorForDB.ts +13 -0
  79. package/src/workflows/coreWorkflows/processors/processors/processor.ts +23 -0
  80. package/src/workflows/coreWorkflows/processors/processors/processorsFunctions.ts +47 -0
  81. package/src/workflows/coreWorkflows/processors/utils.ts +102 -0
  82. package/src/workflows/coreWorkflows/processors/validator/validator.ts +19 -0
  83. package/src/workflows/coreWorkflows/processors/validator/validatorsFunction.ts +52 -0
  84. package/src/workflows/coreWorkflows/queryCoreWorkflows.ts +31 -0
  85. package/src/workflows/coreWorkflows/utilsCoreWorkflows.ts +40 -0
  86. package/src/workflows/coreWorkflows/workflowsCollection/workflowsCollectionWorkflows.ts +101 -0
@@ -0,0 +1,474 @@
1
+ import type { FindAllParamsOutput } from "../../../types/index.ts";
2
+ import { Lobb } from "../../../Lobb.ts";
3
+ import format from "pg-format";
4
+ import _ from "lodash";
5
+ import { LobbError } from "../../../LobbError.ts";
6
+ import { basicFilterOperatorsSchema } from "../../../types/index.ts";
7
+ import { ZodError } from "zod";
8
+
9
+ export class QueryBuilder {
10
+ collectionName: string;
11
+ params: FindAllParamsOutput;
12
+
13
+ static build(collectionName: string, params: FindAllParamsOutput) {
14
+ const builder = new QueryBuilder(collectionName, params);
15
+ return builder.build();
16
+ }
17
+
18
+ static buildTotalCountQuery(
19
+ collectionName: string,
20
+ params: FindAllParamsOutput,
21
+ ) {
22
+ const builder = new QueryBuilder(collectionName, params);
23
+ return builder.buildTotalCountQuery();
24
+ }
25
+
26
+ constructor(collectionName: string, params: FindAllParamsOutput) {
27
+ this.collectionName = collectionName;
28
+ this.params = params;
29
+ }
30
+
31
+ public build(): string {
32
+ const query = format(
33
+ `
34
+ SELECT %s
35
+ FROM %I
36
+ %s
37
+ %s
38
+ %s
39
+ %s
40
+ LIMIT %L
41
+ OFFSET %L
42
+ `,
43
+ this.getSelectFields(),
44
+ this.collectionName,
45
+ this.getJoins(),
46
+ this.getGroupBy(),
47
+ QueryBuilder.getWhere(this.params.filter, this.collectionName),
48
+ this.getSort(),
49
+ this.params.limit,
50
+ this.params.page
51
+ ? (this.params.page - 1) * this.params.limit
52
+ : this.params.offset,
53
+ );
54
+
55
+ return query;
56
+ }
57
+
58
+ public buildTotalCountQuery(): string {
59
+ const query = format(
60
+ `
61
+ SELECT COUNT(*)
62
+ FROM %I
63
+ %s
64
+ %s
65
+ %s
66
+ `,
67
+ this.collectionName,
68
+ this.getJoins(),
69
+ this.getGroupBy(),
70
+ QueryBuilder.getWhere(this.params.filter, this.collectionName),
71
+ );
72
+
73
+ return query;
74
+ }
75
+
76
+ private getSelectFields(): string {
77
+ // TODO: SANITIZE ALL THE FIELDS IN HERE THAT COMES FROM THE USER USING THE FORMAT LIBRARY
78
+ const columns: Set<string> = new Set([`${this.collectionName}.id`]);
79
+ const fields = this.params.fields.split(",").map((f) => f.trim());
80
+
81
+ // Separate main fields (no dot) from foreign fields
82
+ const mainFields = fields.filter((f) => !f.includes("."));
83
+ mainFields.forEach((field) => {
84
+ if (field === "*") {
85
+ const currentCollectionFields = Object.keys(
86
+ Lobb.instance.configManager.getCollection(this.collectionName).fields,
87
+ );
88
+ currentCollectionFields.forEach((item) => {
89
+ columns.add(`${this.collectionName}.${item}`);
90
+ });
91
+ } else {
92
+ columns.add(`${this.collectionName}.${field}`);
93
+ }
94
+ });
95
+
96
+ // Process foreign fields (nested relations)
97
+ if (fields.some((f) => f.includes("."))) {
98
+ const foreignFields: Record<string, string[]> = {};
99
+
100
+ // Group foreign fields by their parent
101
+ for (const field of fields) {
102
+ if (field.includes(".")) {
103
+ const foreignField = field.split(".")[0];
104
+ if (!foreignFields[foreignField]) foreignFields[foreignField] = [];
105
+ }
106
+ }
107
+
108
+ for (const foreignField of Object.keys(foreignFields)) {
109
+ foreignFields[foreignField] = fields
110
+ .filter((f) => f.startsWith(`${foreignField}.`))
111
+ .map((f) => f.slice(foreignField.length + 1));
112
+ }
113
+
114
+ // Add JSON build objects for each foreign field
115
+ for (
116
+ const [foreignField, nestedFields] of Object.entries(foreignFields)
117
+ ) {
118
+ const foreignFieldCollection = this.getForeignFieldCollection(
119
+ this.collectionName,
120
+ foreignField,
121
+ );
122
+
123
+ // Remove raw foreign key column
124
+ columns.delete(`${this.collectionName}.${foreignField}`);
125
+
126
+ // Determine alias (handle self-reference)
127
+ const alias = foreignFieldCollection === this.collectionName
128
+ ? `${foreignFieldCollection}_self_${foreignField}`
129
+ : foreignFieldCollection;
130
+
131
+ // Build JSON safely: only if related row exists
132
+ const jsonField = `
133
+ CASE
134
+ WHEN ${alias}.id IS NULL THEN NULL
135
+ ELSE json_build_object(
136
+ ${nestedFields.map((f) => `'${f}', ${alias}.${f}`).join(", ")}
137
+ )
138
+ END AS ${foreignField}
139
+ `;
140
+
141
+ columns.add(jsonField.trim());
142
+ }
143
+ }
144
+
145
+ return Array.from(columns).join(", ");
146
+ }
147
+
148
+ private getJsonBuildObject(
149
+ collectionName: string,
150
+ fieldName: string,
151
+ columns: string[],
152
+ ): string {
153
+ return `json_build_object(
154
+ ${
155
+ columns.map((column) => `'${column}', ${collectionName}.${column}`)
156
+ .join(",")
157
+ }
158
+ ) AS ${fieldName}`;
159
+ }
160
+
161
+ private getJoins(): string {
162
+ const joins: string[] = [];
163
+ const fields = this.params.fields.split(",").map((field) => field.trim());
164
+ const foreignFields: Set<string> = new Set();
165
+
166
+ // Collect all foreign fields from the selected fields
167
+ for (const field of fields) {
168
+ if (field.includes(".")) {
169
+ const foreignKeyField = field.split(".")[0];
170
+ foreignFields.add(foreignKeyField);
171
+ }
172
+ }
173
+
174
+ const collectionName = this.collectionName;
175
+
176
+ // Build LEFT JOIN statements
177
+ for (const foreignField of Array.from(foreignFields)) {
178
+ const foreignKeyCollection = this.getForeignFieldCollection(
179
+ collectionName,
180
+ foreignField,
181
+ );
182
+
183
+ // If self-referencing, generate a unique alias
184
+ const alias = foreignKeyCollection === collectionName
185
+ ? `${foreignKeyCollection}_self_${foreignField}`
186
+ : foreignKeyCollection;
187
+
188
+ joins.push(
189
+ `LEFT JOIN ${foreignKeyCollection} AS ${alias} ON ${alias}.id = ${collectionName}.${foreignField}`,
190
+ );
191
+ }
192
+
193
+ return joins.join(" ");
194
+ }
195
+
196
+ private getGroupBy(): string {
197
+ return "";
198
+ }
199
+
200
+ private getSort() {
201
+ if (!this.params.sort) return `ORDER BY ${this.collectionName}.id DESC`;
202
+
203
+ const orderByClauses = this.params.sort.split(",").map((field) => {
204
+ field = field.trim();
205
+ if (!field) return "";
206
+
207
+ const direction = field.startsWith("-") ? "DESC" : "ASC";
208
+ const fieldName = field.replace(/^-/, "");
209
+
210
+ return format(`${this.collectionName}.%I %s`, fieldName, direction);
211
+ }).filter(Boolean);
212
+
213
+ return orderByClauses.length ? `ORDER BY ${orderByClauses.join(", ")}` : "";
214
+ }
215
+
216
+ public static getWhere(filterObject: any, collection: string): string {
217
+ const filter = _.cloneDeepWith(
218
+ filterObject,
219
+ (value: any, key: any) => {
220
+ if (typeof key === "string" && !key.includes("$")) {
221
+ // transform `{ title: "test" }` to `{ title: $eq: "test" }`
222
+ if (!_.isPlainObject(value)) {
223
+ value = {
224
+ $eq: value,
225
+ };
226
+ }
227
+
228
+ // validate the schema of the filterComparisonOperators object
229
+ try {
230
+ value = basicFilterOperatorsSchema.parse(value);
231
+ } catch (error) {
232
+ if (error instanceof ZodError) {
233
+ throw new LobbError({
234
+ code: "BAD_REQUEST",
235
+ message: "Invalid filter schema",
236
+ details: error.errors[0],
237
+ });
238
+ }
239
+ throw error;
240
+ }
241
+
242
+ return value;
243
+ }
244
+ },
245
+ );
246
+
247
+ const whereClause = this.buildWhereClause(filter, collection);
248
+
249
+ return whereClause ? `WHERE ${whereClause}` : "";
250
+ }
251
+
252
+ public static buildWhereClause(
253
+ filterObject: any,
254
+ collection: string,
255
+ ): string {
256
+ function columnHandler(collectionName: string, fieldName: string) {
257
+ return `${collectionName}.${format.ident(fieldName)}`;
258
+ }
259
+
260
+ if (!filterObject) {
261
+ return "";
262
+ }
263
+
264
+ const operatorsMap: { [key: string]: string } = {
265
+ "$eq": "=",
266
+ "$ne": "<>",
267
+ "$in": "IN",
268
+ "$nin": "NOT IN",
269
+ "$lt": "<",
270
+ "$lte": "<=",
271
+ "$gt": ">",
272
+ "$gte": ">=",
273
+ "$between": "BETWEEN",
274
+ "$nbetween": "NOT BETWEEN",
275
+ };
276
+
277
+ let whereClause = "";
278
+
279
+ // Function to process each individual condition
280
+ const processCondition = (key: string, value: any): string => {
281
+ if (value && typeof value === "object") {
282
+ const operator = Object.keys(value)[0];
283
+
284
+ if (operator) {
285
+ if (operatorsMap[operator]) {
286
+ const sqlOperator = operatorsMap[operator];
287
+ const conditionValue = value[operator];
288
+
289
+ if (Array.isArray(conditionValue)) {
290
+ if (sqlOperator === "IN" || sqlOperator === "NOT IN") {
291
+ const placeholders = conditionValue.map((val) =>
292
+ format("%L", val)
293
+ )
294
+ .join(", ");
295
+ return `${
296
+ columnHandler(collection, key)
297
+ } ${sqlOperator} (${placeholders})`;
298
+ }
299
+ if (sqlOperator === "BETWEEN" || sqlOperator === "NOT BETWEEN") {
300
+ return `${columnHandler(collection, key)} ${sqlOperator} ${
301
+ format("%L", conditionValue[0])
302
+ } AND ${format("%L", conditionValue[1])}`;
303
+ }
304
+ }
305
+
306
+ if (conditionValue === null) {
307
+ if (operator === "$eq") {
308
+ return `${columnHandler(collection, key)} IS NULL`;
309
+ } else if (operator === "$ne") {
310
+ return `${columnHandler(collection, key)} IS NOT NULL`;
311
+ }
312
+ }
313
+
314
+ // TODO: add the collection identifier to all other conditions so that the select records from boms work in products
315
+ return `${columnHandler(collection, key)} ${sqlOperator} ${
316
+ format("%L", conditionValue)
317
+ }`;
318
+ } else if (operator === "$contains") {
319
+ const conditionValue = value[operator];
320
+ const formattedValue = format("%L", `%${conditionValue}%`);
321
+ const subQuery = `${
322
+ columnHandler(collection, key)
323
+ } LIKE ${formattedValue}`;
324
+ return subQuery;
325
+ } else if (operator === "$icontains") {
326
+ const conditionValue = value[operator];
327
+ const formattedValue = format("%L", `%${conditionValue}%`);
328
+ const subQuery = `${
329
+ columnHandler(collection, key)
330
+ } ILIKE ${formattedValue}`;
331
+ return subQuery;
332
+ } else if (operator === "$ncontains") {
333
+ const conditionValue = value[operator];
334
+ const formattedValue = format("%L", `%${conditionValue}%`);
335
+ const subQuery = `${
336
+ columnHandler(collection, key)
337
+ } NOT LIKE ${formattedValue}`;
338
+ return subQuery;
339
+ } else if (operator === "$incontains") {
340
+ const conditionValue = value[operator];
341
+ const formattedValue = format("%L", `%${conditionValue}%`);
342
+ const subQuery = `${
343
+ columnHandler(collection, key)
344
+ } NOT ILIKE ${formattedValue}`;
345
+ return subQuery;
346
+ } else if (operator === "$starts_with") {
347
+ const conditionValue = value[operator];
348
+ const formattedValue = format("%L", `${conditionValue}%`);
349
+ const subQuery = `${
350
+ columnHandler(collection, key)
351
+ } LIKE ${formattedValue}`;
352
+ return subQuery;
353
+ } else if (operator === "$istarts_with") {
354
+ const conditionValue = value[operator];
355
+ const formattedValue = format("%L", `${conditionValue}%`);
356
+ const subQuery = `${
357
+ columnHandler(collection, key)
358
+ } ILIKE ${formattedValue}`;
359
+ return subQuery;
360
+ } else if (operator === "$nstarts_with") {
361
+ const conditionValue = value[operator];
362
+ const formattedValue = format("%L", `${conditionValue}%`);
363
+ const subQuery = `${
364
+ columnHandler(collection, key)
365
+ } NOT LIKE ${formattedValue}`;
366
+ return subQuery;
367
+ } else if (operator === "$instarts_with") {
368
+ const conditionValue = value[operator];
369
+ const formattedValue = format("%L", `${conditionValue}%`);
370
+ const subQuery = `${
371
+ columnHandler(collection, key)
372
+ } NOT ILIKE ${formattedValue}`;
373
+ return subQuery;
374
+ } else if (operator === "$ends_with") {
375
+ const conditionValue = value[operator];
376
+ const formattedValue = format("%L", `%${conditionValue}`);
377
+ const subQuery = `${
378
+ columnHandler(collection, key)
379
+ } LIKE ${formattedValue}`;
380
+ return subQuery;
381
+ } else if (operator === "$iends_with") {
382
+ const conditionValue = value[operator];
383
+ const formattedValue = format("%L", `%${conditionValue}`);
384
+ const subQuery = `${
385
+ columnHandler(collection, key)
386
+ } ILIKE ${formattedValue}`;
387
+ return subQuery;
388
+ } else if (operator === "$nends_with") {
389
+ const conditionValue = value[operator];
390
+ const formattedValue = format("%L", `%${conditionValue}`);
391
+ const subQuery = `${
392
+ columnHandler(collection, key)
393
+ } NOT LIKE ${formattedValue}`;
394
+ return subQuery;
395
+ } else if (operator === "$inends_with") {
396
+ const conditionValue = value[operator];
397
+ const formattedValue = format("%L", `%${conditionValue}`);
398
+ const subQuery = `${
399
+ columnHandler(collection, key)
400
+ } NOT ILIKE ${formattedValue}`;
401
+ return subQuery;
402
+ } else if (operator === "$regex") {
403
+ const conditionValue = value[operator];
404
+ const formattedValue = format("%L", conditionValue);
405
+ const subQuery = `${
406
+ columnHandler(collection, key)
407
+ } ~ ${formattedValue}`;
408
+ return subQuery;
409
+ } else {
410
+ throw new LobbError({
411
+ code: "INTERNAL_SERVER_ERROR",
412
+ message: `The (${operator}) operator is not supported`,
413
+ });
414
+ }
415
+ }
416
+
417
+ const sql = this.buildWhereClause(value, collection);
418
+ return sql ? `(${sql})` : "";
419
+ }
420
+
421
+ return `${columnHandler(collection, key)} = ${format("%L", value)}`;
422
+ };
423
+
424
+ if (filterObject.$and) {
425
+ const andConditions = filterObject.$and.map((condition: any) => {
426
+ const sql = this.buildWhereClause(condition, collection);
427
+ return sql ? `(${sql})` : "";
428
+ }).filter(Boolean).join(" AND ");
429
+ whereClause += andConditions;
430
+ } else if (filterObject.$or) {
431
+ const orConditions = filterObject.$or.map((condition: any) => {
432
+ const sql = this.buildWhereClause(condition, collection);
433
+ return sql ? `(${sql})` : "";
434
+ }).filter(Boolean).join(" OR ");
435
+ whereClause += orConditions;
436
+ } else {
437
+ Object.keys(filterObject).forEach((key) => {
438
+ if (key !== "$and" && key !== "$or") {
439
+ const condition = processCondition(key, filterObject[key]);
440
+ if (condition) {
441
+ whereClause += ` ${condition} AND`;
442
+ }
443
+ }
444
+ });
445
+ whereClause = whereClause.replace(/ AND$/, "");
446
+ }
447
+
448
+ return whereClause.trim();
449
+ }
450
+
451
+ getForeignFieldCollection(collection: string, foreignField: string) {
452
+ const relations = Lobb.instance.configManager.config.relations;
453
+ if (!relations) {
454
+ throw new LobbError({
455
+ code: "BAD_REQUEST",
456
+ message:
457
+ `Can't use dot (".") operator in (fields) findAll property because there is not relations`,
458
+ });
459
+ }
460
+ const foreignFieldRelation = relations.find((relation) =>
461
+ relation.from.collection === collection &&
462
+ relation.from.field === foreignField
463
+ );
464
+ if (!foreignFieldRelation) {
465
+ throw new LobbError({
466
+ code: "BAD_REQUEST",
467
+ message:
468
+ `Couldn't find the relation object of the (${this.collectionName}.${foreignField}) field`,
469
+ });
470
+ }
471
+
472
+ return foreignFieldRelation.to.collection;
473
+ }
474
+ }
@@ -0,0 +1,6 @@
1
+ export function fieldsHasId(findAllFields: string): boolean {
2
+ return findAllFields
3
+ .split(",")
4
+ .map((f) => f.trim())
5
+ .some((f) => f === "id" || f === "*");
6
+ }
@@ -0,0 +1,191 @@
1
+ import type { ZodObject } from "zod";
2
+ import { Lobb } from "../Lobb.ts";
3
+ import { getCoreEvents } from "./coreEvents/index.ts";
4
+ import { LobbError } from "../LobbError.ts";
5
+ import { isValidCron } from "cron-validator";
6
+ import { CronJob } from "cron";
7
+ import { LobbReturnError } from "../api/errorHandler.ts";
8
+ import { getStudioEvents } from "./studioEvents/index.ts";
9
+
10
+ // Event type
11
+ export interface Event {
12
+ name: string;
13
+ inputSchema: ZodObject<any>;
14
+ outputSchema: ZodObject<any>;
15
+ }
16
+
17
+ // Subscription type
18
+ type AnyFunction = (...args: any[]) => any;
19
+ export interface EventContext {
20
+ eventName: string;
21
+ workflows: Record<string, AnyFunction>;
22
+ LobbError: typeof LobbError;
23
+ lobb: Lobb;
24
+ }
25
+ type Handler = (input: any, ctx: EventContext) => Promise<any>;
26
+ export interface Subscription {
27
+ name: string;
28
+ eventName: string;
29
+ handler: Handler;
30
+ }
31
+
32
+ export class EventSystem {
33
+ public events: Event[] = [];
34
+ private subscriptions: Subscription[] = [];
35
+ private crons: Record<string, CronJob> = {};
36
+
37
+ public async init() {
38
+ this.registerCoreEvents();
39
+ this.registerStudioEvents();
40
+ }
41
+
42
+ public getEvent(eventName: string): Event {
43
+ const event = this.events.find((event) => event.name === eventName);
44
+ if (!event) {
45
+ throw new LobbError({
46
+ code: "BAD_REQUEST",
47
+ message: `An event with the (${eventName}) name doesnt exist`,
48
+ });
49
+ }
50
+ return event;
51
+ }
52
+
53
+ public register(event: Event) {
54
+ this.events.push(event);
55
+ }
56
+
57
+ public subscribe(subscription: Subscription) {
58
+ if (this.subscriptionExist(subscription.name)) {
59
+ throw new LobbError({
60
+ code: "BAD_REQUEST",
61
+ message:
62
+ `The subscription with the (${subscription.name}) name already exists`,
63
+ });
64
+ }
65
+ if (isValidCron(subscription.eventName)) {
66
+ const eventContext = this.getEventContext(subscription.eventName);
67
+ const job = CronJob.from({
68
+ cronTime: subscription.eventName,
69
+ onTick: async function () {
70
+ try {
71
+ await subscription.handler({}, eventContext);
72
+ } catch (error) {
73
+ console.error("CRON WORKFLOW ERROR:");
74
+ console.error(error);
75
+ }
76
+ },
77
+ start: true,
78
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
79
+ });
80
+ this.crons[subscription.name] = job;
81
+ } else {
82
+ if (!this.eventExists(subscription.eventName)) {
83
+ throw new LobbError({
84
+ code: "BAD_REQUEST",
85
+ message:
86
+ `Trying to subscribe to the (${subscription.eventName}) event that doesnt exist`,
87
+ });
88
+ }
89
+ this.subscriptions.push(subscription);
90
+ }
91
+ }
92
+
93
+ public async unsubscribe(subscriptionName: string) {
94
+ this.subscriptions = this.subscriptions.filter(
95
+ (sub) => sub.name !== subscriptionName,
96
+ );
97
+ if (this.crons[subscriptionName]) {
98
+ await this.crons[subscriptionName].stop();
99
+ delete this.crons[subscriptionName];
100
+ }
101
+ }
102
+
103
+ public subscriptionExist(subscriptionName: string) {
104
+ return this.subscriptions.some(
105
+ (subscription) => subscription.name === subscriptionName,
106
+ );
107
+ }
108
+
109
+ public async emit(eventName: string, input: Record<string, any>) {
110
+ if (!this.eventExists(eventName)) {
111
+ throw new Error(
112
+ `Trying to emit with the (${eventName}) event that doesnt exist`,
113
+ );
114
+ }
115
+
116
+ const subscriptions = this.subscriptions.filter((subscription) => {
117
+ return eventName.startsWith(subscription.eventName);
118
+ });
119
+
120
+ if (!subscriptions.length) {
121
+ return input;
122
+ }
123
+
124
+ let output;
125
+ for (let index = 0; index < subscriptions.length; index++) {
126
+ const subscription = subscriptions[index];
127
+ try {
128
+ const localOutput = await subscription.handler(
129
+ input,
130
+ this.getEventContext(subscription.eventName),
131
+ );
132
+
133
+ output = localOutput;
134
+ } catch (error) {
135
+ if (error instanceof Response) {
136
+ throw new LobbReturnError(error);
137
+ }
138
+
139
+ throw error;
140
+ }
141
+ }
142
+
143
+ return output;
144
+ }
145
+
146
+ public getEventContext(eventName: string): EventContext {
147
+ return {
148
+ eventName: eventName,
149
+ workflows: Object.fromEntries(
150
+ Lobb.instance.workflowSystem.workflows.map((wf) => [
151
+ wf.name,
152
+ wf.handler,
153
+ ]),
154
+ ),
155
+ LobbError: LobbError,
156
+ lobb: Lobb.instance,
157
+ };
158
+ }
159
+
160
+ public eventHasSubscribers(eventName: string) {
161
+ const subscriptions = this.subscriptions.filter((subscription) => {
162
+ return eventName.startsWith(subscription.eventName);
163
+ });
164
+ return Boolean(subscriptions.length);
165
+ }
166
+
167
+ public eventExists(eventName: string) {
168
+ return this.events.some((event) => event.name === eventName);
169
+ }
170
+
171
+ public close() {
172
+ this.events = [];
173
+ this.subscriptions = [];
174
+ }
175
+
176
+ public registerCoreEvents() {
177
+ const coreEvents = getCoreEvents();
178
+ for (let index = 0; index < coreEvents.length; index++) {
179
+ const coreEvent = coreEvents[index];
180
+ this.register(coreEvent);
181
+ }
182
+ }
183
+
184
+ public registerStudioEvents() {
185
+ const coreEvents = getStudioEvents();
186
+ for (let index = 0; index < coreEvents.length; index++) {
187
+ const coreEvent = coreEvents[index];
188
+ this.register(coreEvent);
189
+ }
190
+ }
191
+ }