@lobb-js/core 0.23.0 → 0.25.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 +1 -1
- package/src/Lobb.ts +1 -0
- package/src/api/collections/CollectionControllers.ts +17 -0
- package/src/api/collections/collectionStore.ts +688 -24
- package/src/api/meta/service.ts +1 -0
- package/src/config/ConfigManager.ts +131 -7
- package/src/config/validations.ts +13 -5
- package/src/database/drivers/pgDriver/QueryBuilder.ts +3 -141
- package/src/index.ts +1 -1
- package/src/types/collectionServiceSchema.ts +61 -14
- package/src/types/config/collectionFields.ts +18 -0
- package/src/types/config/collectionsConfig.ts +5 -0
- package/src/types/config/relations.ts +22 -3
- package/src/types/index.ts +7 -3
- package/src/workflows/coreWorkflows/processors/hooksWorkflows.ts +29 -5
package/src/api/meta/service.ts
CHANGED
|
@@ -40,6 +40,7 @@ export class MetaService {
|
|
|
40
40
|
collections[collectionName].singleton = Lobb.instance.configManager
|
|
41
41
|
.isCollectionSingleton(collectionName);
|
|
42
42
|
|
|
43
|
+
// filling the collection properties
|
|
43
44
|
// is collection virtual
|
|
44
45
|
collections[collectionName].virtual = Lobb.instance.configManager
|
|
45
46
|
.isCollectionVirtual(collectionName);
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "../types/index.ts";
|
|
8
8
|
import type { CollectionField } from "../types/index.ts";
|
|
9
9
|
import type { Extension } from "../types/index.ts";
|
|
10
|
-
import type { RelationsConfig } from "../types/index.ts";
|
|
10
|
+
import type { RelationsConfig, RegularRelation } from "../types/index.ts";
|
|
11
11
|
|
|
12
12
|
import _ from "lodash";
|
|
13
13
|
import { coreCollections } from "../coreCollections/index.ts";
|
|
@@ -42,6 +42,39 @@ export class ConfigManager {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
public expandPolymorphicFields() {
|
|
46
|
+
for (const [collectionName, collection] of Object.entries(this.config.collections)) {
|
|
47
|
+
if (!collection.fields) continue;
|
|
48
|
+
for (const [fieldName, fieldValue] of Object.entries(collection.fields)) {
|
|
49
|
+
if (fieldValue.type !== "polymorphic") continue;
|
|
50
|
+
|
|
51
|
+
const { collection_field, id_field, references } = fieldValue as any;
|
|
52
|
+
|
|
53
|
+
// Create the two real DB columns (hidden in UI)
|
|
54
|
+
collection.fields[collection_field] = {
|
|
55
|
+
type: "string",
|
|
56
|
+
length: 50,
|
|
57
|
+
ui: { hidden: true },
|
|
58
|
+
} as any;
|
|
59
|
+
collection.fields[id_field] = {
|
|
60
|
+
type: "integer",
|
|
61
|
+
ui: { hidden: true },
|
|
62
|
+
} as any;
|
|
63
|
+
|
|
64
|
+
// Convert the original field to virtual (drives the UI picker)
|
|
65
|
+
collection.fields[fieldName] = { virtual: true, ui: { icon: "Shuffle" } } as any;
|
|
66
|
+
|
|
67
|
+
// Auto-register the polymorphic relation
|
|
68
|
+
if (!this.config.relations) this.config.relations = [];
|
|
69
|
+
this.config.relations.push({
|
|
70
|
+
type: "polymorphic",
|
|
71
|
+
from: { collection: collectionName, virtual_field: fieldName, collection_field, id_field },
|
|
72
|
+
to: references,
|
|
73
|
+
} as any);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
45
78
|
public addUniqueIndexesFromFields() {
|
|
46
79
|
for (
|
|
47
80
|
const [collectionName, collectionValue] of Object.entries(
|
|
@@ -54,7 +87,7 @@ export class ConfigManager {
|
|
|
54
87
|
(collectionValue as any).fields,
|
|
55
88
|
)
|
|
56
89
|
) {
|
|
57
|
-
if (fieldValue.unique) {
|
|
90
|
+
if ((fieldValue as any).unique) {
|
|
58
91
|
const indexName = `${collectionName}_${fieldName}_unique_index`;
|
|
59
92
|
(collectionValue as any).indexes[indexName] = {
|
|
60
93
|
unique: true,
|
|
@@ -290,12 +323,103 @@ export class ConfigManager {
|
|
|
290
323
|
delete this.config.collections[collectionName];
|
|
291
324
|
}
|
|
292
325
|
|
|
293
|
-
public
|
|
326
|
+
public getParentRelation(
|
|
327
|
+
collectionName: string,
|
|
328
|
+
fieldName: string,
|
|
329
|
+
): RegularRelation | null {
|
|
330
|
+
return (
|
|
331
|
+
this.config.relations?.find((rel): rel is RegularRelation => {
|
|
332
|
+
if ("type" in rel && rel.type === "polymorphic") return false;
|
|
333
|
+
return (
|
|
334
|
+
(rel as RegularRelation).from.collection === collectionName &&
|
|
335
|
+
(rel as RegularRelation).from.field === fieldName
|
|
336
|
+
);
|
|
337
|
+
}) ?? null
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
public getPolymorphicRelationByVirtualField(
|
|
342
|
+
collectionName: string,
|
|
343
|
+
virtualField: string,
|
|
344
|
+
): { collectionField: string; idField: string; allowedCollections: string[] } | null {
|
|
345
|
+
const rel = this.config.relations?.find((r) => {
|
|
346
|
+
if (!("type" in r) || r.type !== "polymorphic") return false;
|
|
347
|
+
return (r as any).from.collection === collectionName &&
|
|
348
|
+
(r as any).from.virtual_field === virtualField;
|
|
349
|
+
}) as any | undefined;
|
|
350
|
+
|
|
351
|
+
if (!rel) return null;
|
|
352
|
+
return {
|
|
353
|
+
collectionField: rel.from.collection_field,
|
|
354
|
+
idField: rel.from.id_field,
|
|
355
|
+
allowedCollections: rel.to,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
public getPolymorphicChildRelation(
|
|
360
|
+
parentCollection: string,
|
|
361
|
+
childCollection: string,
|
|
362
|
+
): { collectionField: string; idField: string } | null {
|
|
363
|
+
const rel = this.config.relations?.find((r) => {
|
|
364
|
+
if (!("type" in r) || r.type !== "polymorphic") return false;
|
|
365
|
+
return (r as any).from.collection === childCollection &&
|
|
366
|
+
(r as any).to.includes(parentCollection);
|
|
367
|
+
}) as any | undefined;
|
|
368
|
+
|
|
369
|
+
if (!rel) return null;
|
|
370
|
+
return {
|
|
371
|
+
collectionField: rel.from.collection_field,
|
|
372
|
+
idField: rel.from.id_field,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
public getM2MJunction(
|
|
377
|
+
fromCollection: string,
|
|
378
|
+
toCollection: string,
|
|
379
|
+
): { junctionCollection: string; parentFKField: string; targetFKField: string } | null {
|
|
380
|
+
const fromChildRelations = this.getChildRelations(fromCollection);
|
|
381
|
+
|
|
382
|
+
for (const fromRelation of fromChildRelations) {
|
|
383
|
+
const junctionName = fromRelation.from.collection;
|
|
384
|
+
if (!this.config.collections[junctionName]?.junction) continue;
|
|
385
|
+
|
|
386
|
+
const toRelation = this.config.relations?.find((rel): rel is RegularRelation => {
|
|
387
|
+
if ("type" in rel && rel.type === "polymorphic") return false;
|
|
388
|
+
return (
|
|
389
|
+
(rel as RegularRelation).from.collection === junctionName &&
|
|
390
|
+
(rel as RegularRelation).to.collection === toCollection &&
|
|
391
|
+
(rel as RegularRelation).from.field !== fromRelation.from.field
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
if (toRelation) {
|
|
396
|
+
return {
|
|
397
|
+
junctionCollection: junctionName,
|
|
398
|
+
parentFKField: fromRelation.from.field,
|
|
399
|
+
targetFKField: toRelation.from.field,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
public getCollectionFKFields(collectionName: string): string[] {
|
|
408
|
+
if (!this.config.relations) return [];
|
|
409
|
+
return this.config.relations
|
|
410
|
+
.filter((rel): rel is RegularRelation => {
|
|
411
|
+
if ("type" in rel && rel.type === "polymorphic") return false;
|
|
412
|
+
return (rel as RegularRelation).from.collection === collectionName;
|
|
413
|
+
})
|
|
414
|
+
.map((rel) => rel.from.field);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
public getChildRelations(collectionName: string): RegularRelation[] {
|
|
294
418
|
if (this.config.relations) {
|
|
295
|
-
|
|
296
|
-
relation.
|
|
297
|
-
|
|
298
|
-
|
|
419
|
+
return this.config.relations.filter((relation): relation is RegularRelation => {
|
|
420
|
+
if ("type" in relation && relation.type === "polymorphic") return false;
|
|
421
|
+
return (relation as RegularRelation).to.collection === collectionName;
|
|
422
|
+
});
|
|
299
423
|
}
|
|
300
424
|
|
|
301
425
|
return [];
|
|
@@ -10,11 +10,19 @@ function validateRelations(config: Config, configManager: ConfigManager) {
|
|
|
10
10
|
if (config.relations) {
|
|
11
11
|
for (let index = 0; index < config.relations.length; index++) {
|
|
12
12
|
const relation = config.relations[index];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
configManager.
|
|
17
|
-
|
|
13
|
+
if ("type" in relation && relation.type === "polymorphic") {
|
|
14
|
+
configManager.getCollection(relation.from.collection);
|
|
15
|
+
configManager.fieldExists(relation.from.collection_field, relation.from.collection);
|
|
16
|
+
configManager.fieldExists(relation.from.id_field, relation.from.collection);
|
|
17
|
+
for (const targetCollection of relation.to) {
|
|
18
|
+
configManager.getCollection(targetCollection);
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
configManager.getCollection(relation.from.collection);
|
|
22
|
+
configManager.getField((relation.from as any).field, relation.from.collection);
|
|
23
|
+
const to = relation.to as { collection: string; field: string };
|
|
24
|
+
configManager.getCollection(to.collection);
|
|
25
|
+
configManager.getField(to.field, to.collection);
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { FindAllParamsOutput } from "../../../types/index.ts";
|
|
2
|
-
import { Lobb } from "../../../Lobb.ts";
|
|
3
2
|
import format from "pg-format";
|
|
4
3
|
import _ from "lodash";
|
|
5
4
|
import { LobbError } from "../../../LobbError.ts";
|
|
@@ -74,128 +73,14 @@ export class QueryBuilder {
|
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
private getSelectFields(): string {
|
|
77
|
-
// TODO: SANITIZE ALL THE FIELDS IN HERE THAT COMES FROM THE USER USING THE FORMAT LIBRARY
|
|
78
76
|
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 collectionFields = Lobb.instance.configManager.getNormalCollection(this.collectionName).fields;
|
|
86
|
-
const currentCollectionFields = Object.entries(collectionFields)
|
|
87
|
-
.filter(([, fieldConfig]) => !fieldConfig.virtual)
|
|
88
|
-
.map(([fieldName]) => fieldName);
|
|
89
|
-
currentCollectionFields.forEach((item) => {
|
|
90
|
-
columns.add(`${this.collectionName}.${item}`);
|
|
91
|
-
});
|
|
92
|
-
} else {
|
|
93
|
-
columns.add(`${this.collectionName}.${field}`);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Process foreign fields (nested relations)
|
|
98
|
-
if (fields.some((f) => f.includes("."))) {
|
|
99
|
-
const foreignFields: Record<string, string[]> = {};
|
|
100
|
-
|
|
101
|
-
// Group foreign fields by their parent
|
|
102
|
-
for (const field of fields) {
|
|
103
|
-
if (field.includes(".")) {
|
|
104
|
-
const foreignField = field.split(".")[0];
|
|
105
|
-
if (!foreignFields[foreignField]) foreignFields[foreignField] = [];
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
for (const foreignField of Object.keys(foreignFields)) {
|
|
110
|
-
foreignFields[foreignField] = fields
|
|
111
|
-
.filter((f) => f.startsWith(`${foreignField}.`))
|
|
112
|
-
.map((f) => f.slice(foreignField.length + 1));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Add JSON build objects for each foreign field
|
|
116
|
-
for (
|
|
117
|
-
const [foreignField, nestedFields] of Object.entries(foreignFields)
|
|
118
|
-
) {
|
|
119
|
-
const foreignFieldCollection = this.getForeignFieldCollection(
|
|
120
|
-
this.collectionName,
|
|
121
|
-
foreignField,
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
// Remove raw foreign key column
|
|
125
|
-
columns.delete(`${this.collectionName}.${foreignField}`);
|
|
126
|
-
|
|
127
|
-
// Determine alias (handle self-reference)
|
|
128
|
-
const alias = foreignFieldCollection === this.collectionName
|
|
129
|
-
? `${foreignFieldCollection}_self_${foreignField}`
|
|
130
|
-
: foreignFieldCollection;
|
|
131
|
-
|
|
132
|
-
// Exclude virtual fields — they have no DB column in the related table
|
|
133
|
-
const foreignCollectionFields = Lobb.instance.configManager.getNormalCollection(foreignFieldCollection).fields;
|
|
134
|
-
const nonVirtualNestedFields = nestedFields.filter((f) => !foreignCollectionFields[f]?.virtual);
|
|
135
|
-
|
|
136
|
-
// Build JSON safely: only if related row exists
|
|
137
|
-
const jsonField = `
|
|
138
|
-
CASE
|
|
139
|
-
WHEN ${alias}.id IS NULL THEN NULL
|
|
140
|
-
ELSE json_build_object(
|
|
141
|
-
${nonVirtualNestedFields.map((f) => `'${f}', ${alias}.${f}`).join(", ")}
|
|
142
|
-
)
|
|
143
|
-
END AS ${foreignField}
|
|
144
|
-
`;
|
|
145
|
-
|
|
146
|
-
columns.add(jsonField.trim());
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
77
|
+
const fields = this.params.fields.split(",").map((f) => f.trim()).filter(Boolean);
|
|
78
|
+
fields.forEach((field) => columns.add(`${this.collectionName}.${field}`));
|
|
150
79
|
return Array.from(columns).join(", ");
|
|
151
80
|
}
|
|
152
81
|
|
|
153
|
-
private getJsonBuildObject(
|
|
154
|
-
collectionName: string,
|
|
155
|
-
fieldName: string,
|
|
156
|
-
columns: string[],
|
|
157
|
-
): string {
|
|
158
|
-
return `json_build_object(
|
|
159
|
-
${
|
|
160
|
-
columns.map((column) => `'${column}', ${collectionName}.${column}`)
|
|
161
|
-
.join(",")
|
|
162
|
-
}
|
|
163
|
-
) AS ${fieldName}`;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
82
|
private getJoins(): string {
|
|
167
|
-
|
|
168
|
-
const fields = this.params.fields.split(",").map((field) => field.trim());
|
|
169
|
-
const foreignFields: Set<string> = new Set();
|
|
170
|
-
|
|
171
|
-
// Collect all foreign fields from the selected fields
|
|
172
|
-
for (const field of fields) {
|
|
173
|
-
if (field.includes(".")) {
|
|
174
|
-
const foreignKeyField = field.split(".")[0];
|
|
175
|
-
foreignFields.add(foreignKeyField);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const collectionName = this.collectionName;
|
|
180
|
-
|
|
181
|
-
// Build LEFT JOIN statements
|
|
182
|
-
for (const foreignField of Array.from(foreignFields)) {
|
|
183
|
-
const foreignKeyCollection = this.getForeignFieldCollection(
|
|
184
|
-
collectionName,
|
|
185
|
-
foreignField,
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
// If self-referencing, generate a unique alias
|
|
189
|
-
const alias = foreignKeyCollection === collectionName
|
|
190
|
-
? `${foreignKeyCollection}_self_${foreignField}`
|
|
191
|
-
: foreignKeyCollection;
|
|
192
|
-
|
|
193
|
-
joins.push(
|
|
194
|
-
`LEFT JOIN ${foreignKeyCollection} AS ${alias} ON ${alias}.id = ${collectionName}.${foreignField}`,
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return joins.join(" ");
|
|
83
|
+
return "";
|
|
199
84
|
}
|
|
200
85
|
|
|
201
86
|
private getGroupBy(): string {
|
|
@@ -453,27 +338,4 @@ export class QueryBuilder {
|
|
|
453
338
|
return whereClause.trim();
|
|
454
339
|
}
|
|
455
340
|
|
|
456
|
-
getForeignFieldCollection(collection: string, foreignField: string) {
|
|
457
|
-
const relations = Lobb.instance.configManager.config.relations;
|
|
458
|
-
if (!relations) {
|
|
459
|
-
throw new LobbError({
|
|
460
|
-
code: "BAD_REQUEST",
|
|
461
|
-
message:
|
|
462
|
-
`Can't use dot (".") operator in (fields) findAll property because there is not relations`,
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
const foreignFieldRelation = relations.find((relation) =>
|
|
466
|
-
relation.from.collection === collection &&
|
|
467
|
-
relation.from.field === foreignField
|
|
468
|
-
);
|
|
469
|
-
if (!foreignFieldRelation) {
|
|
470
|
-
throw new LobbError({
|
|
471
|
-
code: "BAD_REQUEST",
|
|
472
|
-
message:
|
|
473
|
-
`Couldn't find the relation object of the (${this.collectionName}.${foreignField}) field`,
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return foreignFieldRelation.to.collection;
|
|
478
|
-
}
|
|
479
341
|
}
|
package/src/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ export {
|
|
|
40
40
|
filterSchema,
|
|
41
41
|
} from "./types/filterSchema.ts";
|
|
42
42
|
export type {
|
|
43
|
-
|
|
43
|
+
FindAllParams,
|
|
44
44
|
FindAllParamsOutput,
|
|
45
45
|
} from "./types/collectionServiceSchema.ts";
|
|
46
46
|
export { findAllParamsSchema } from "./types/collectionServiceSchema.ts";
|
|
@@ -1,18 +1,65 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
|
|
3
|
+
export type ChildParams = {
|
|
4
|
+
fields?: string | string[];
|
|
5
|
+
sort?: string;
|
|
6
|
+
limit?: number;
|
|
7
|
+
offset?: number;
|
|
8
|
+
page?: number;
|
|
9
|
+
filter?: any;
|
|
10
|
+
children?: Record<string, ChildParams>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const childParamsSchema: z.ZodType<ChildParams> = z.lazy(() =>
|
|
14
|
+
z.object({
|
|
15
|
+
fields: z.union([z.string(), z.array(z.string())]).optional(),
|
|
5
16
|
sort: z.string().optional(),
|
|
6
|
-
limit: z.
|
|
7
|
-
offset: z.
|
|
8
|
-
page: z.
|
|
9
|
-
fields: z.string().default("*"),
|
|
17
|
+
limit: z.number().optional(),
|
|
18
|
+
offset: z.number().optional(),
|
|
19
|
+
page: z.number().optional(),
|
|
10
20
|
filter: z.any(),
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
children: z.record(childParamsSchema).optional(),
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const baseParamsSchema = z.object({
|
|
26
|
+
fields: z.union([z.string(), z.array(z.string())]).optional(),
|
|
27
|
+
children: z.record(childParamsSchema).optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const findOneParamsSchema = baseParamsSchema;
|
|
31
|
+
export type FindOneParams = z.infer<typeof findOneParamsSchema>;
|
|
32
|
+
|
|
33
|
+
export const findAllParamsSchema = baseParamsSchema
|
|
34
|
+
.extend({
|
|
35
|
+
sort: z.string().optional(),
|
|
36
|
+
limit: z.number().optional(),
|
|
37
|
+
offset: z.number().optional(),
|
|
38
|
+
page: z.number().optional(),
|
|
39
|
+
filter: z.any(),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
export type FindAllParams = z.infer<typeof findAllParamsSchema>;
|
|
43
|
+
|
|
44
|
+
export type CreateChildren = Record<string, {
|
|
45
|
+
create?: Array<Record<string, any> & { children?: CreateChildren }>;
|
|
46
|
+
link?: (string | number)[];
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
export type UpdateChildren = Record<string, {
|
|
50
|
+
create?: Array<Record<string, any> & { children?: UpdateChildren }>;
|
|
51
|
+
link?: (string | number)[];
|
|
52
|
+
unlink?: (string | number)[];
|
|
53
|
+
delete?: (string | number)[];
|
|
54
|
+
set?: (string | number)[];
|
|
55
|
+
update?: Array<{ id: string | number } & Record<string, any>>;
|
|
56
|
+
}>;
|
|
57
|
+
|
|
58
|
+
export type FindAllParamsOutput = {
|
|
59
|
+
sort?: string;
|
|
60
|
+
limit: number;
|
|
61
|
+
offset: number;
|
|
62
|
+
page?: number;
|
|
63
|
+
fields: string;
|
|
64
|
+
filter?: any;
|
|
65
|
+
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { RelationCollectionFieldSchema } from "./relations.ts";
|
|
2
2
|
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import type { icons } from "lucide-svelte";
|
|
5
|
+
|
|
6
|
+
type LucideIconName = keyof typeof icons;
|
|
4
7
|
|
|
5
8
|
export type EnumColor =
|
|
6
9
|
| "red" | "rose" | "pink" | "fuchsia" | "purple"
|
|
@@ -28,8 +31,10 @@ const UiInputSchema = z.object({
|
|
|
28
31
|
// Defining the ui property schema
|
|
29
32
|
const UiSchema = z.object({
|
|
30
33
|
disabled: z.boolean().optional(),
|
|
34
|
+
hidden: z.boolean().optional(),
|
|
31
35
|
placeholder: z.string().optional(),
|
|
32
36
|
input: UiInputSchema.optional(),
|
|
37
|
+
icon: z.custom<LucideIconName>((val) => typeof val === "string").optional(),
|
|
33
38
|
});
|
|
34
39
|
|
|
35
40
|
export type FieldHookFn = (ctx: {
|
|
@@ -164,6 +169,18 @@ export const CollectionTimeFieldSchema = z.union([
|
|
|
164
169
|
VirtualFieldSchema.extend({ type: z.literal("time") }),
|
|
165
170
|
]);
|
|
166
171
|
|
|
172
|
+
// Polymorphic field — expands during config init into:
|
|
173
|
+
// - {collection_field}: real string column (hidden in UI)
|
|
174
|
+
// - {id_field}: real integer column (hidden in UI)
|
|
175
|
+
// - the field itself becomes virtual (drives the compound picker widget)
|
|
176
|
+
// A polymorphic relation is also auto-registered into config.relations.
|
|
177
|
+
export const CollectionPolymorphicFieldSchema = CollectionFieldBaseSchema.extend({
|
|
178
|
+
type: z.literal("polymorphic"),
|
|
179
|
+
collection_field: z.string(),
|
|
180
|
+
id_field: z.string(),
|
|
181
|
+
references: z.array(z.string()),
|
|
182
|
+
});
|
|
183
|
+
|
|
167
184
|
// Union type if you want a single schema for all
|
|
168
185
|
export const CollectionFieldSchema = z.union([
|
|
169
186
|
CollectionBoolFieldSchema,
|
|
@@ -176,6 +193,7 @@ export const CollectionFieldSchema = z.union([
|
|
|
176
193
|
CollectionStringFieldSchema,
|
|
177
194
|
CollectionTextFieldSchema,
|
|
178
195
|
CollectionTimeFieldSchema,
|
|
196
|
+
CollectionPolymorphicFieldSchema,
|
|
179
197
|
]);
|
|
180
198
|
|
|
181
199
|
export type CollectionField = z.infer<typeof CollectionFieldSchema>;
|
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
CollectionFieldSchema,
|
|
4
4
|
CollectionIntegerFieldSchema,
|
|
5
5
|
} from "./collectionFields.ts";
|
|
6
|
+
import type { icons } from "lucide-svelte";
|
|
7
|
+
|
|
8
|
+
type LucideIconName = keyof typeof icons;
|
|
6
9
|
|
|
7
10
|
export type CollectionHookFn = (ctx: {
|
|
8
11
|
data: Record<string, any>;
|
|
@@ -49,6 +52,7 @@ const CollectionTabSchema = z.object({
|
|
|
49
52
|
});
|
|
50
53
|
|
|
51
54
|
const CollectionUiSchema = z.object({
|
|
55
|
+
icon: z.custom<LucideIconName>((val) => typeof val === "string").optional(),
|
|
52
56
|
tabs: z.array(CollectionTabSchema).optional(),
|
|
53
57
|
}).optional();
|
|
54
58
|
|
|
@@ -64,6 +68,7 @@ export const NormalCollectionConfigSchema = z.object({
|
|
|
64
68
|
virtual: z.literal(false).optional(),
|
|
65
69
|
category: z.string().optional(),
|
|
66
70
|
singleton: z.boolean().optional(),
|
|
71
|
+
junction: z.boolean().optional(),
|
|
67
72
|
hooks: CollectionHooksSchema,
|
|
68
73
|
indexes: CollectionIndexesSchema.optional(),
|
|
69
74
|
fields: CollectionFieldsSchema,
|
|
@@ -6,12 +6,31 @@ export const RelationCollectionFieldSchema = z.object({
|
|
|
6
6
|
field: z.string(),
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
// Regular relation schema
|
|
10
|
-
const
|
|
9
|
+
// Regular (FK) relation schema
|
|
10
|
+
export const RegularRelationSchema = z.object({
|
|
11
11
|
from: RelationCollectionFieldSchema,
|
|
12
12
|
to: RelationCollectionFieldSchema,
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
export type RegularRelation = z.infer<typeof RegularRelationSchema>;
|
|
16
|
+
|
|
17
|
+
// Polymorphic relation schema
|
|
18
|
+
// from.collection_field holds the target collection name per row
|
|
19
|
+
// from.id_field holds the target record id per row
|
|
20
|
+
// to lists the allowed target collections
|
|
21
|
+
const PolymorphicRelationSchema = z.object({
|
|
22
|
+
type: z.literal("polymorphic"),
|
|
23
|
+
from: z.object({
|
|
24
|
+
collection: z.string(),
|
|
25
|
+
virtual_field: z.string(),
|
|
26
|
+
collection_field: z.string(),
|
|
27
|
+
id_field: z.string(),
|
|
28
|
+
}),
|
|
29
|
+
to: z.array(z.string()),
|
|
30
|
+
});
|
|
31
|
+
|
|
15
32
|
// Union of all relation types
|
|
16
|
-
export const RelationsConfigSchema = z.array(
|
|
33
|
+
export const RelationsConfigSchema = z.array(
|
|
34
|
+
z.union([RegularRelationSchema, PolymorphicRelationSchema]),
|
|
35
|
+
);
|
|
17
36
|
export type RelationsConfig = z.infer<typeof RelationsConfigSchema>;
|
package/src/types/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { DatabaseDriver } from "./DatabaseDriver.ts";
|
|
|
2
2
|
export type { FindAllResult, MassReturn } from "./DatabaseDriver.ts";
|
|
3
3
|
export type { ApiCollectionAction } from "./apiSchema.ts";
|
|
4
4
|
export type { Config, WebConfig } from "./config/config.ts";
|
|
5
|
-
export type { RelationsConfig } from "./config/relations.ts";
|
|
5
|
+
export type { RelationsConfig, RegularRelation } from "./config/relations.ts";
|
|
6
6
|
export type {
|
|
7
7
|
CollectionConfig,
|
|
8
8
|
NormalCollectionConfig,
|
|
@@ -24,10 +24,14 @@ export {
|
|
|
24
24
|
filterSchema,
|
|
25
25
|
} from "./filterSchema.ts";
|
|
26
26
|
export type {
|
|
27
|
-
|
|
27
|
+
FindAllParams,
|
|
28
28
|
FindAllParamsOutput,
|
|
29
|
+
FindOneParams,
|
|
30
|
+
ChildParams,
|
|
31
|
+
CreateChildren,
|
|
32
|
+
UpdateChildren,
|
|
29
33
|
} from "./collectionServiceSchema.ts";
|
|
30
|
-
export { findAllParamsSchema } from "./collectionServiceSchema.ts";
|
|
34
|
+
export { findAllParamsSchema, findOneParamsSchema, childParamsSchema } from "./collectionServiceSchema.ts";
|
|
31
35
|
export { CollectionConfigSchema } from "./config/collectionsConfig.ts";
|
|
32
36
|
export type { CollectionControllers } from "./CollectionControllers.ts";
|
|
33
37
|
export type {
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { Workflow } from "../../WorkflowSystem.ts";
|
|
2
2
|
import { Lobb } from "../../../Lobb.ts";
|
|
3
|
+
import { LobbError } from "../../../LobbError.ts";
|
|
4
|
+
|
|
5
|
+
async function runHook(hook: Function, args: any, label: string) {
|
|
6
|
+
try {
|
|
7
|
+
return await hook(args);
|
|
8
|
+
} catch (err: any) {
|
|
9
|
+
throw new LobbError({
|
|
10
|
+
code: "BAD_REQUEST",
|
|
11
|
+
message: `Hook "${label}" threw an error: ${err?.message ?? String(err)}`,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
3
15
|
|
|
4
16
|
async function runFieldHooks(
|
|
5
17
|
hookName: "beforeCreate" | "beforeUpdate" | "afterCreate" | "afterUpdate",
|
|
@@ -10,7 +22,11 @@ async function runFieldHooks(
|
|
|
10
22
|
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
|
11
23
|
const hook = "hooks" in fieldConfig ? fieldConfig.hooks?.[hookName] : undefined;
|
|
12
24
|
if (!hook) continue;
|
|
13
|
-
const result = await
|
|
25
|
+
const result = await runHook(
|
|
26
|
+
hook,
|
|
27
|
+
{ data: input.data, context: input.context },
|
|
28
|
+
`${input.collectionName}.${fieldName}.${hookName}`,
|
|
29
|
+
);
|
|
14
30
|
if (result !== undefined) {
|
|
15
31
|
input.data[fieldName] = result;
|
|
16
32
|
}
|
|
@@ -24,7 +40,9 @@ export const hooksWorkflows: Workflow[] = [
|
|
|
24
40
|
handler: async (input) => {
|
|
25
41
|
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
|
|
26
42
|
const hooks = Lobb.instance.configManager.getNormalCollection(input.collectionName).hooks;
|
|
27
|
-
|
|
43
|
+
if (hooks?.beforeCreate) {
|
|
44
|
+
await runHook(hooks.beforeCreate, { data: input.data, context: input.context }, `${input.collectionName}.beforeCreate`);
|
|
45
|
+
}
|
|
28
46
|
await runFieldHooks("beforeCreate", input);
|
|
29
47
|
return input;
|
|
30
48
|
},
|
|
@@ -35,7 +53,9 @@ export const hooksWorkflows: Workflow[] = [
|
|
|
35
53
|
handler: async (input) => {
|
|
36
54
|
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
|
|
37
55
|
const hooks = Lobb.instance.configManager.getNormalCollection(input.collectionName).hooks;
|
|
38
|
-
|
|
56
|
+
if (hooks?.beforeUpdate) {
|
|
57
|
+
await runHook(hooks.beforeUpdate, { data: input.data, context: input.context }, `${input.collectionName}.beforeUpdate`);
|
|
58
|
+
}
|
|
39
59
|
await runFieldHooks("beforeUpdate", input);
|
|
40
60
|
return input;
|
|
41
61
|
},
|
|
@@ -46,7 +66,9 @@ export const hooksWorkflows: Workflow[] = [
|
|
|
46
66
|
handler: async (input) => {
|
|
47
67
|
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
|
|
48
68
|
const hooks = Lobb.instance.configManager.getNormalCollection(input.collectionName).hooks;
|
|
49
|
-
|
|
69
|
+
if (hooks?.afterCreate) {
|
|
70
|
+
await runHook(hooks.afterCreate, { data: input.data, context: input.context }, `${input.collectionName}.afterCreate`);
|
|
71
|
+
}
|
|
50
72
|
await runFieldHooks("afterCreate", input);
|
|
51
73
|
return input;
|
|
52
74
|
},
|
|
@@ -57,7 +79,9 @@ export const hooksWorkflows: Workflow[] = [
|
|
|
57
79
|
handler: async (input) => {
|
|
58
80
|
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
|
|
59
81
|
const hooks = Lobb.instance.configManager.getNormalCollection(input.collectionName).hooks;
|
|
60
|
-
|
|
82
|
+
if (hooks?.afterUpdate) {
|
|
83
|
+
await runHook(hooks.afterUpdate, { data: input.data, context: input.context }, `${input.collectionName}.afterUpdate`);
|
|
84
|
+
}
|
|
61
85
|
await runFieldHooks("afterUpdate", input);
|
|
62
86
|
return input;
|
|
63
87
|
},
|