@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.
- package/package.json +48 -0
- package/src/Lobb.ts +150 -0
- package/src/LobbError.ts +105 -0
- package/src/TypesGenerator.ts +11 -0
- package/src/api/WebServer.ts +126 -0
- package/src/api/collections/CollectionControllers.ts +485 -0
- package/src/api/collections/CollectionService.ts +162 -0
- package/src/api/collections/collectionRoutes.ts +105 -0
- package/src/api/collections/collectionStore.ts +647 -0
- package/src/api/collections/transactions.ts +166 -0
- package/src/api/collections/utils.ts +73 -0
- package/src/api/errorHandler.ts +73 -0
- package/src/api/events/index.ts +129 -0
- package/src/api/meta/route.ts +66 -0
- package/src/api/meta/service.ts +163 -0
- package/src/api/middlewares.ts +71 -0
- package/src/api/openApiRoute.ts +1017 -0
- package/src/api/schema/SchemaService.ts +71 -0
- package/src/api/schema/schemaRoutes.ts +13 -0
- package/src/config/ConfigManager.ts +252 -0
- package/src/config/validations.ts +49 -0
- package/src/coreCollections/collectionsCollection.ts +56 -0
- package/src/coreCollections/index.ts +14 -0
- package/src/coreCollections/migrationsCollection.ts +36 -0
- package/src/coreCollections/queryCollection.ts +26 -0
- package/src/coreCollections/workflowsCollection.ts +73 -0
- package/src/coreDbSetup/index.ts +72 -0
- package/src/coreMigrations/index.ts +3 -0
- package/src/database/DatabaseService.ts +44 -0
- package/src/database/DatabaseSyncManager.ts +173 -0
- package/src/database/MigrationsManager.ts +95 -0
- package/src/database/drivers/MongoDriver.ts +750 -0
- package/src/database/drivers/pgDriver/PGDriver.ts +655 -0
- package/src/database/drivers/pgDriver/QueryBuilder.ts +474 -0
- package/src/database/drivers/pgDriver/utils.ts +6 -0
- package/src/events/EventSystem.ts +191 -0
- package/src/events/coreEvents/index.ts +218 -0
- package/src/events/studioEvents/index.ts +32 -0
- package/src/extension/ExtensionSystem.ts +236 -0
- package/src/extension/dashboardRoute.ts +35 -0
- package/src/fields/ArrayField.ts +33 -0
- package/src/fields/BoolField.ts +34 -0
- package/src/fields/DateField.ts +13 -0
- package/src/fields/DateTimeField.ts +13 -0
- package/src/fields/DecimalField.ts +13 -0
- package/src/fields/FieldUtils.ts +56 -0
- package/src/fields/FloatField.ts +13 -0
- package/src/fields/IntegerField.ts +13 -0
- package/src/fields/LongField.ts +13 -0
- package/src/fields/ObjectField.ts +15 -0
- package/src/fields/StringField.ts +13 -0
- package/src/fields/TextField.ts +13 -0
- package/src/fields/TimeField.ts +13 -0
- package/src/index.ts +53 -0
- package/src/studio/Studio.ts +108 -0
- package/src/types/CollectionControllers.ts +15 -0
- package/src/types/DatabaseDriver.ts +115 -0
- package/src/types/Extension.ts +46 -0
- package/src/types/Field.ts +29 -0
- package/src/types/apiSchema.ts +12 -0
- package/src/types/collectionServiceSchema.ts +18 -0
- package/src/types/config/collectionFields.ts +85 -0
- package/src/types/config/collectionsConfig.ts +50 -0
- package/src/types/config/config.ts +66 -0
- package/src/types/config/relations.ts +17 -0
- package/src/types/filterSchema.ts +88 -0
- package/src/types/index.ts +38 -0
- package/src/types/migrations.ts +12 -0
- package/src/types/websockets.ts +34 -0
- package/src/types/workflows/processors.ts +1 -0
- package/src/utils/lockCollectionToObject.ts +204 -0
- package/src/utils/utils.ts +310 -0
- package/src/workflows/WorkflowSystem.ts +182 -0
- package/src/workflows/coreWorkflows/collectionsTable/index.ts +118 -0
- package/src/workflows/coreWorkflows/index.ts +18 -0
- package/src/workflows/coreWorkflows/processors/postOperationsWorkflows.ts +46 -0
- package/src/workflows/coreWorkflows/processors/preOperationsWorkflows.ts +27 -0
- package/src/workflows/coreWorkflows/processors/processorForDB.ts +13 -0
- package/src/workflows/coreWorkflows/processors/processors/processor.ts +23 -0
- package/src/workflows/coreWorkflows/processors/processors/processorsFunctions.ts +47 -0
- package/src/workflows/coreWorkflows/processors/utils.ts +102 -0
- package/src/workflows/coreWorkflows/processors/validator/validator.ts +19 -0
- package/src/workflows/coreWorkflows/processors/validator/validatorsFunction.ts +52 -0
- package/src/workflows/coreWorkflows/queryCoreWorkflows.ts +31 -0
- package/src/workflows/coreWorkflows/utilsCoreWorkflows.ts +40 -0
- 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,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
|
+
}
|