@lobb-js/core 0.17.0 → 0.19.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 +2 -2
- package/src/api/collections/collectionStore.ts +9 -5
- package/src/api/collections/utils.ts +2 -1
- package/src/api/meta/service.ts +9 -2
- package/src/config/ConfigManager.ts +46 -10
- package/src/config/validations.ts +1 -1
- package/src/database/DatabaseSyncManager.ts +7 -2
- package/src/database/drivers/pgDriver/PGDriver.ts +28 -3
- package/src/database/drivers/pgDriver/QueryBuilder.ts +9 -4
- package/src/types/DatabaseDriver.ts +5 -0
- package/src/types/config/collectionFields.ts +92 -51
- package/src/types/config/collectionsConfig.ts +30 -3
- package/src/types/index.ts +1 -0
- package/src/workflows/coreWorkflows/processors/defaultWorkflows.ts +2 -1
- package/src/workflows/coreWorkflows/processors/enumWorkflows.ts +2 -1
- package/src/workflows/coreWorkflows/processors/hooksWorkflows.ts +11 -6
- package/src/workflows/coreWorkflows/processors/processors/processor.ts +1 -1
- package/src/workflows/coreWorkflows/processors/requiredWorkflows.ts +3 -2
- package/src/workflows/coreWorkflows/processors/validator/validator.ts +1 -1
- package/src/workflows/coreWorkflows/processors/validatorWorkflows.ts +3 -2
package/package.json
CHANGED
|
@@ -226,11 +226,15 @@ export class CollectionStore {
|
|
|
226
226
|
transaction: client,
|
|
227
227
|
},
|
|
228
228
|
);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
this.
|
|
232
|
-
|
|
233
|
-
|
|
229
|
+
|
|
230
|
+
if (!Lobb.instance.configManager.isCollectionVirtual(collectionName)) {
|
|
231
|
+
data = await this.dbDriver.createOne(
|
|
232
|
+
collectionName,
|
|
233
|
+
this.filterPayload(collectionName, result.data),
|
|
234
|
+
client,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
234
238
|
data = (await Lobb.instance.eventSystem.emit(
|
|
235
239
|
`core.store.createOne`,
|
|
236
240
|
{
|
|
@@ -8,6 +8,7 @@ export function getCollectionDocumentSchema(
|
|
|
8
8
|
const collection = Lobb.instance.configManager.getCollection(
|
|
9
9
|
collectionName,
|
|
10
10
|
);
|
|
11
|
+
if (collection.virtual) return z.object({}).passthrough();
|
|
11
12
|
const fieldNames = Object.keys(collection.fields);
|
|
12
13
|
|
|
13
14
|
const properties: Record<string, any> = {};
|
|
@@ -64,7 +65,7 @@ export function getColFieldPropSchema(
|
|
|
64
65
|
);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
const requiredField = Boolean(field.validators?.required);
|
|
68
|
+
const requiredField = Boolean("validators" in field && field.validators?.required);
|
|
68
69
|
if (!requiredField) {
|
|
69
70
|
fieldSchema = fieldSchema.optional();
|
|
70
71
|
}
|
package/src/api/meta/service.ts
CHANGED
|
@@ -40,6 +40,10 @@ export class MetaService {
|
|
|
40
40
|
collections[collectionName].singleton = Lobb.instance.configManager
|
|
41
41
|
.isCollectionSingleton(collectionName);
|
|
42
42
|
|
|
43
|
+
// is collection virtual
|
|
44
|
+
collections[collectionName].virtual = Lobb.instance.configManager
|
|
45
|
+
.isCollectionVirtual(collectionName);
|
|
46
|
+
|
|
43
47
|
// filling the extension property
|
|
44
48
|
collections[collectionName].category = collection.category;
|
|
45
49
|
collections[collectionName].owner = Lobb.instance.utils
|
|
@@ -64,12 +68,15 @@ export class MetaService {
|
|
|
64
68
|
field.label = fieldName;
|
|
65
69
|
field.key = fieldName;
|
|
66
70
|
field.enum = "enum" in fieldConfig ? fieldConfig.enum : undefined;
|
|
67
|
-
field.validators = fieldConfig.validators;
|
|
68
|
-
field.pre_processors = fieldConfig.pre_processors;
|
|
71
|
+
field.validators = "validators" in fieldConfig ? fieldConfig.validators : undefined;
|
|
72
|
+
field.pre_processors = "pre_processors" in fieldConfig ? fieldConfig.pre_processors : undefined;
|
|
69
73
|
field.ui = fieldConfig.ui;
|
|
70
74
|
|
|
71
75
|
collections[collectionName].fields[fieldName] = field;
|
|
72
76
|
}
|
|
77
|
+
|
|
78
|
+
// filling the collection ui
|
|
79
|
+
collections[collectionName].ui = collection.ui;
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
return collections;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type CollectionConfig,
|
|
4
4
|
CollectionConfigSchema,
|
|
5
5
|
type CollectionsConfig,
|
|
6
|
+
type NormalCollectionConfig,
|
|
6
7
|
} from "../types/index.ts";
|
|
7
8
|
import type { CollectionField } from "../types/index.ts";
|
|
8
9
|
import type { Extension } from "../types/index.ts";
|
|
@@ -47,9 +48,10 @@ export class ConfigManager {
|
|
|
47
48
|
this.config.collections,
|
|
48
49
|
)
|
|
49
50
|
) {
|
|
51
|
+
if (collectionValue.virtual) continue;
|
|
50
52
|
for (
|
|
51
53
|
const [fieldName, fieldValue] of Object.entries(
|
|
52
|
-
this.config.collections[collectionName].fields,
|
|
54
|
+
(this.config.collections[collectionName] as any).fields,
|
|
53
55
|
)
|
|
54
56
|
) {
|
|
55
57
|
if (fieldValue.type === "integer" && fieldValue.references) {
|
|
@@ -82,6 +84,12 @@ export class ConfigManager {
|
|
|
82
84
|
|
|
83
85
|
// Iterate over the collections and map each field to only include the 'type' property
|
|
84
86
|
for (const collectionName in collections) {
|
|
87
|
+
// Virtual collections have no DB table — skip them entirely
|
|
88
|
+
if (collections[collectionName].virtual) {
|
|
89
|
+
delete collections[collectionName];
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
85
93
|
// Keep only the 'indexes' and 'fields' properties in a collection
|
|
86
94
|
collections[collectionName] = {
|
|
87
95
|
indexes: collections[collectionName].indexes,
|
|
@@ -89,6 +97,12 @@ export class ConfigManager {
|
|
|
89
97
|
};
|
|
90
98
|
const fields = collections[collectionName].fields;
|
|
91
99
|
for (const fieldName in fields) {
|
|
100
|
+
// virtual fields have no database column — skip them entirely
|
|
101
|
+
if (fields[fieldName].virtual) {
|
|
102
|
+
delete fields[fieldName];
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
// including only the properties that are db related
|
|
93
107
|
const field: any = {};
|
|
94
108
|
field["type"] = fields[fieldName].type;
|
|
@@ -125,19 +139,30 @@ export class ConfigManager {
|
|
|
125
139
|
return collection;
|
|
126
140
|
}
|
|
127
141
|
|
|
142
|
+
public getNormalCollection(collectionName: string): NormalCollectionConfig {
|
|
143
|
+
const collection = this.getCollection(collectionName);
|
|
144
|
+
if (collection.virtual) {
|
|
145
|
+
throw new LobbError({
|
|
146
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
147
|
+
message: `The (${collectionName}) collection is virtual and has no fields`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return collection;
|
|
151
|
+
}
|
|
152
|
+
|
|
128
153
|
public getFieldNames(collectionName: string) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
);
|
|
154
|
+
const collection = this.getCollection(collectionName);
|
|
155
|
+
if (collection.virtual) return [];
|
|
156
|
+
return Object.keys(collection.fields);
|
|
132
157
|
}
|
|
133
158
|
|
|
134
159
|
public fieldExists(
|
|
135
160
|
fieldName: string,
|
|
136
161
|
collectionName: string,
|
|
137
162
|
): boolean {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
);
|
|
163
|
+
const collection = this.getCollection(collectionName);
|
|
164
|
+
if (collection.virtual) return false;
|
|
165
|
+
return Boolean(collection.fields[fieldName]);
|
|
141
166
|
}
|
|
142
167
|
|
|
143
168
|
public collectionExists(collectionName: string): boolean {
|
|
@@ -148,7 +173,8 @@ export class ConfigManager {
|
|
|
148
173
|
fieldName: string,
|
|
149
174
|
collectionName: string,
|
|
150
175
|
): CollectionField {
|
|
151
|
-
const
|
|
176
|
+
const collection = this.getCollection(collectionName);
|
|
177
|
+
const field = collection.virtual ? undefined : collection.fields[fieldName];
|
|
152
178
|
if (!field) {
|
|
153
179
|
throw new LobbError({
|
|
154
180
|
code: "BAD_REQUEST",
|
|
@@ -199,7 +225,11 @@ export class ConfigManager {
|
|
|
199
225
|
const virtualCollections: CollectionsConfig = {};
|
|
200
226
|
const collections = this.getCollections();
|
|
201
227
|
for (const [collectionName, collection] of Object.entries(collections)) {
|
|
202
|
-
|
|
228
|
+
if (collection.virtual) {
|
|
229
|
+
virtualCollections[collectionName] = collection;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const collectionFieldsNames = Object.keys(collection.fields ?? {});
|
|
203
233
|
if (collectionFieldsNames.length <= 1) {
|
|
204
234
|
virtualCollections[collectionName] = collection;
|
|
205
235
|
}
|
|
@@ -247,6 +277,12 @@ export class ConfigManager {
|
|
|
247
277
|
}
|
|
248
278
|
|
|
249
279
|
public isCollectionSingleton(collectionName: string): boolean {
|
|
250
|
-
|
|
280
|
+
const collection = this.config.collections[collectionName];
|
|
281
|
+
if (!collection || collection.virtual) return false;
|
|
282
|
+
return Boolean(collection.singleton);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
public isCollectionVirtual(collectionName: string): boolean {
|
|
286
|
+
return Boolean(this.config.collections[collectionName]?.virtual);
|
|
251
287
|
}
|
|
252
288
|
}
|
|
@@ -37,7 +37,7 @@ function validateDBNamesAreSnakeCase(
|
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
for (
|
|
40
|
-
const [fieldName, _] of Object.entries(collectionValue.fields)
|
|
40
|
+
const [fieldName, _] of Object.entries(collectionValue.virtual ? {} : collectionValue.fields)
|
|
41
41
|
) {
|
|
42
42
|
if (!isSnakeCase(fieldName)) {
|
|
43
43
|
throw new Error(
|
|
@@ -111,6 +111,11 @@ export class DatabaseSyncManager {
|
|
|
111
111
|
const collectionName = change.path[0] as string;
|
|
112
112
|
const fieldName = change.path[2] as string;
|
|
113
113
|
await this.dbDriver.removeField(collectionName, fieldName);
|
|
114
|
+
} else if (change.path[1] === "fields" && change.op === "replace") {
|
|
115
|
+
const collectionName = change.path[0] as string;
|
|
116
|
+
const fieldName = change.path[2] as string;
|
|
117
|
+
const fieldConfig = Lobb.instance.configManager.getNormalCollection(collectionName).fields[fieldName];
|
|
118
|
+
await this.dbDriver.alterField(collectionName, fieldName, fieldConfig);
|
|
114
119
|
} else if (change.path[1] === "indexes" && change.op === "add") {
|
|
115
120
|
const collectionName = change.path[0] as string;
|
|
116
121
|
const indexName = change.path[2] as string;
|
|
@@ -126,10 +131,10 @@ export class DatabaseSyncManager {
|
|
|
126
131
|
} else if (change.path[1] === "indexes" && change.op === "replace") {
|
|
127
132
|
const collectionName = change.path[0] as string;
|
|
128
133
|
const indexName = change.path[2] as string;
|
|
129
|
-
const collectionConfig = Lobb.instance.configManager.
|
|
134
|
+
const collectionConfig = Lobb.instance.configManager.getNormalCollection(
|
|
130
135
|
collectionName,
|
|
131
136
|
);
|
|
132
|
-
const indexes = collectionConfig.indexes;
|
|
137
|
+
const indexes = collectionConfig.indexes ?? {};
|
|
133
138
|
const index = indexes[indexName];
|
|
134
139
|
await this.dbDriver.dropIndex(collectionName, indexName);
|
|
135
140
|
await this.dbDriver.createIndex(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import format from "pg-format";
|
|
2
2
|
import type { FindAllParamsOutput } from "../../../types/index.ts";
|
|
3
3
|
import type {
|
|
4
|
-
|
|
4
|
+
NormalCollectionConfig,
|
|
5
5
|
CollectionIndex,
|
|
6
6
|
CollectionIndexes,
|
|
7
7
|
CollectionsConfig,
|
|
@@ -109,7 +109,7 @@ export class PGDriver extends DatabaseDriver {
|
|
|
109
109
|
|
|
110
110
|
public async createCollection(
|
|
111
111
|
collectionName: string,
|
|
112
|
-
collectionConfig:
|
|
112
|
+
collectionConfig: NormalCollectionConfig,
|
|
113
113
|
) {
|
|
114
114
|
const collectionEntries = Object.entries(collectionConfig.fields);
|
|
115
115
|
const createTableBodySql: string = collectionEntries.map(
|
|
@@ -127,7 +127,7 @@ export class PGDriver extends DatabaseDriver {
|
|
|
127
127
|
await this.runQuery(client, query);
|
|
128
128
|
|
|
129
129
|
// creating the indexes
|
|
130
|
-
const indexes = collectionConfig.indexes;
|
|
130
|
+
const indexes = collectionConfig.indexes ?? {};
|
|
131
131
|
for (
|
|
132
132
|
const [indexName, value] of Object.entries(indexes)
|
|
133
133
|
) {
|
|
@@ -169,6 +169,18 @@ export class PGDriver extends DatabaseDriver {
|
|
|
169
169
|
await this.runQuery(client, sql);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
public async alterField(
|
|
173
|
+
collectionName: string,
|
|
174
|
+
fieldName: string,
|
|
175
|
+
field: CollectionField,
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
const client = asDisposable(await this.pool.connect());
|
|
178
|
+
using _ = client;
|
|
179
|
+
const pgType = this.generateFieldSignature(fieldName, field);
|
|
180
|
+
const sql = `ALTER TABLE ${collectionName} ALTER COLUMN ${fieldName} TYPE ${pgType} USING ${fieldName}::${pgType}`;
|
|
181
|
+
await this.runQuery(client, sql);
|
|
182
|
+
}
|
|
183
|
+
|
|
172
184
|
public async getDbSchema(): Promise<CollectionsConfig> {
|
|
173
185
|
const client = asDisposable(await this.pool.connect());
|
|
174
186
|
using _ = client;
|
|
@@ -391,6 +403,7 @@ export class PGDriver extends DatabaseDriver {
|
|
|
391
403
|
localClient?: PoolClient,
|
|
392
404
|
) {
|
|
393
405
|
delete data.id;
|
|
406
|
+
data = this.stripVirtualFields(collectionName, data);
|
|
394
407
|
|
|
395
408
|
const query = format(
|
|
396
409
|
`INSERT INTO ${collectionName} (%I) VALUES (%L) RETURNING *`,
|
|
@@ -409,12 +422,24 @@ export class PGDriver extends DatabaseDriver {
|
|
|
409
422
|
return result.rows[0] as any;
|
|
410
423
|
}
|
|
411
424
|
|
|
425
|
+
private stripVirtualFields(collectionName: string, data: any): any {
|
|
426
|
+
const collection = Lobb.instance.configManager.getCollection(collectionName);
|
|
427
|
+
if (collection.virtual) return data;
|
|
428
|
+
const fields = collection.fields;
|
|
429
|
+
const result = { ...data };
|
|
430
|
+
for (const key of Object.keys(result)) {
|
|
431
|
+
if (fields[key]?.virtual) delete result[key];
|
|
432
|
+
}
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
|
|
412
436
|
public async updateOne(
|
|
413
437
|
collectionName: string,
|
|
414
438
|
id: string,
|
|
415
439
|
data: any,
|
|
416
440
|
localClient?: PoolClient,
|
|
417
441
|
) {
|
|
442
|
+
data = this.stripVirtualFields(collectionName, data);
|
|
418
443
|
const setClause = Object.keys(data).map((key) =>
|
|
419
444
|
format("%I = %L", key, data[key])
|
|
420
445
|
).join(", ");
|
|
@@ -82,9 +82,10 @@ export class QueryBuilder {
|
|
|
82
82
|
const mainFields = fields.filter((f) => !f.includes("."));
|
|
83
83
|
mainFields.forEach((field) => {
|
|
84
84
|
if (field === "*") {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
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);
|
|
88
89
|
currentCollectionFields.forEach((item) => {
|
|
89
90
|
columns.add(`${this.collectionName}.${item}`);
|
|
90
91
|
});
|
|
@@ -128,12 +129,16 @@ export class QueryBuilder {
|
|
|
128
129
|
? `${foreignFieldCollection}_self_${foreignField}`
|
|
129
130
|
: foreignFieldCollection;
|
|
130
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
|
+
|
|
131
136
|
// Build JSON safely: only if related row exists
|
|
132
137
|
const jsonField = `
|
|
133
138
|
CASE
|
|
134
139
|
WHEN ${alias}.id IS NULL THEN NULL
|
|
135
140
|
ELSE json_build_object(
|
|
136
|
-
${
|
|
141
|
+
${nonVirtualNestedFields.map((f) => `'${f}', ${alias}.${f}`).join(", ")}
|
|
137
142
|
)
|
|
138
143
|
END AS ${foreignField}
|
|
139
144
|
`;
|
|
@@ -50,6 +50,11 @@ export abstract class DatabaseDriver {
|
|
|
50
50
|
collectionName: string,
|
|
51
51
|
fieldName: string,
|
|
52
52
|
): Promise<void>;
|
|
53
|
+
public abstract alterField(
|
|
54
|
+
collectionName: string,
|
|
55
|
+
fieldName: string,
|
|
56
|
+
field: CollectionField,
|
|
57
|
+
): Promise<void>;
|
|
53
58
|
public abstract getDbSchema(): Promise<CollectionsConfig>;
|
|
54
59
|
public abstract getIdsByFilter(
|
|
55
60
|
collectionName: string,
|
|
@@ -46,6 +46,7 @@ const FieldHooksSchema = z.object({
|
|
|
46
46
|
|
|
47
47
|
// Base field schema (no default — each specialized schema adds it with the correct type)
|
|
48
48
|
export const CollectionFieldBaseSchema = z.object({
|
|
49
|
+
virtual: z.never().optional(),
|
|
49
50
|
required: z.boolean().optional(),
|
|
50
51
|
hooks: FieldHooksSchema,
|
|
51
52
|
validator: z.custom<FieldValidatorFn>((val) => typeof val === "function").optional(),
|
|
@@ -57,67 +58,107 @@ export const CollectionFieldBaseSchema = z.object({
|
|
|
57
58
|
|
|
58
59
|
export type CollectionFieldBase = z.infer<typeof CollectionFieldBaseSchema>;
|
|
59
60
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// Virtual field schema — read-only, computed at runtime, no DB column.
|
|
62
|
+
// Only type and ui are allowed; all write-side properties are excluded.
|
|
63
|
+
const VirtualFieldSchema = z.object({
|
|
64
|
+
virtual: z.literal(true),
|
|
65
|
+
ui: UiSchema.optional(),
|
|
64
66
|
});
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
// Specialized field schemas
|
|
69
|
+
export const CollectionBoolFieldSchema = z.union([
|
|
70
|
+
CollectionFieldBaseSchema.extend({
|
|
71
|
+
type: z.literal("bool"),
|
|
72
|
+
default: z.boolean().optional(),
|
|
73
|
+
}),
|
|
74
|
+
VirtualFieldSchema.extend({ type: z.literal("bool") }),
|
|
75
|
+
]);
|
|
71
76
|
|
|
72
|
-
export const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
export const CollectionDateFieldSchema = z.union([
|
|
78
|
+
CollectionFieldBaseSchema.extend({
|
|
79
|
+
type: z.literal("date"),
|
|
80
|
+
default: z.union([z.string(), z.date()]).optional(),
|
|
81
|
+
enum: z.array(z.union([z.string(), z.date()])).optional(),
|
|
82
|
+
}),
|
|
83
|
+
VirtualFieldSchema.extend({ type: z.literal("date") }),
|
|
84
|
+
]);
|
|
77
85
|
|
|
78
|
-
export const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
export const CollectionDateTimeFieldSchema = z.union([
|
|
87
|
+
CollectionFieldBaseSchema.extend({
|
|
88
|
+
type: z.literal("datetime"),
|
|
89
|
+
default: z.union([z.string(), z.date()]).optional(),
|
|
90
|
+
enum: z.array(z.union([z.string(), z.date()])).optional(),
|
|
91
|
+
}),
|
|
92
|
+
VirtualFieldSchema.extend({ type: z.literal("datetime") }),
|
|
93
|
+
]);
|
|
83
94
|
|
|
84
|
-
export const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
export const CollectionDecimalFieldSchema = z.union([
|
|
96
|
+
CollectionFieldBaseSchema.extend({
|
|
97
|
+
type: z.literal("decimal"),
|
|
98
|
+
default: z.number().optional(),
|
|
99
|
+
enum: z.array(z.number()).optional(),
|
|
100
|
+
}),
|
|
101
|
+
VirtualFieldSchema.extend({ type: z.literal("decimal") }),
|
|
102
|
+
]);
|
|
89
103
|
|
|
90
|
-
export const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
})
|
|
104
|
+
export const CollectionFloatFieldSchema = z.union([
|
|
105
|
+
CollectionFieldBaseSchema.extend({
|
|
106
|
+
type: z.literal("float"),
|
|
107
|
+
default: z.number().optional(),
|
|
108
|
+
enum: z.array(z.number()).optional(),
|
|
109
|
+
}),
|
|
110
|
+
VirtualFieldSchema.extend({ type: z.literal("float") }),
|
|
111
|
+
]);
|
|
96
112
|
|
|
97
|
-
export const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
export const CollectionIntegerFieldSchema = z.union([
|
|
114
|
+
CollectionFieldBaseSchema.extend({
|
|
115
|
+
type: z.literal("integer"),
|
|
116
|
+
default: z.number().int().optional(),
|
|
117
|
+
enum: z.array(z.number().int()).optional(),
|
|
118
|
+
references: RelationCollectionFieldSchema.optional(),
|
|
119
|
+
}),
|
|
120
|
+
VirtualFieldSchema.extend({ type: z.literal("integer") }),
|
|
121
|
+
]);
|
|
102
122
|
|
|
103
|
-
export const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
})
|
|
123
|
+
export const CollectionLongFieldSchema = z.union([
|
|
124
|
+
CollectionFieldBaseSchema.extend({
|
|
125
|
+
type: z.literal("long"),
|
|
126
|
+
default: z.number().optional(),
|
|
127
|
+
enum: z.array(z.number()).optional(),
|
|
128
|
+
}),
|
|
129
|
+
VirtualFieldSchema.extend({ type: z.literal("long") }),
|
|
130
|
+
]);
|
|
109
131
|
|
|
110
|
-
export const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
export const CollectionStringFieldSchema = z.union([
|
|
133
|
+
CollectionFieldBaseSchema.extend({
|
|
134
|
+
type: z.literal("string"),
|
|
135
|
+
length: z.number(),
|
|
136
|
+
default: z.string().optional(),
|
|
137
|
+
enum: StringEnumSchema.optional(),
|
|
138
|
+
}),
|
|
139
|
+
VirtualFieldSchema.extend({
|
|
140
|
+
type: z.literal("string"),
|
|
141
|
+
length: z.number().optional(),
|
|
142
|
+
}),
|
|
143
|
+
]);
|
|
115
144
|
|
|
116
|
-
export const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
145
|
+
export const CollectionTextFieldSchema = z.union([
|
|
146
|
+
CollectionFieldBaseSchema.extend({
|
|
147
|
+
type: z.literal("text"),
|
|
148
|
+
default: z.string().optional(),
|
|
149
|
+
enum: StringEnumSchema.optional(),
|
|
150
|
+
}),
|
|
151
|
+
VirtualFieldSchema.extend({ type: z.literal("text") }),
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
export const CollectionTimeFieldSchema = z.union([
|
|
155
|
+
CollectionFieldBaseSchema.extend({
|
|
156
|
+
type: z.literal("time"),
|
|
157
|
+
default: z.string().optional(),
|
|
158
|
+
enum: StringEnumSchema.optional(),
|
|
159
|
+
}),
|
|
160
|
+
VirtualFieldSchema.extend({ type: z.literal("time") }),
|
|
161
|
+
]);
|
|
121
162
|
|
|
122
163
|
// Union type if you want a single schema for all
|
|
123
164
|
export const CollectionFieldSchema = z.union([
|
|
@@ -41,15 +41,40 @@ export const CollectionFieldsSchema = z.intersection(
|
|
|
41
41
|
z.record(CollectionFieldSchema),
|
|
42
42
|
);
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const CollectionTabSchema = z.object({
|
|
45
|
+
id: z.string(),
|
|
46
|
+
label: z.string(),
|
|
47
|
+
filter: z.record(z.unknown()).optional(),
|
|
48
|
+
default: z.boolean().optional(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const CollectionUiSchema = z.object({
|
|
52
|
+
tabs: z.array(CollectionTabSchema).optional(),
|
|
53
|
+
}).optional();
|
|
54
|
+
|
|
55
|
+
// Virtual collection — no DB table, no fields, just an API endpoint for workflows to intercept
|
|
56
|
+
export const VirtualCollectionConfigSchema = z.object({
|
|
57
|
+
virtual: z.literal(true),
|
|
58
|
+
category: z.string().optional(),
|
|
59
|
+
ui: CollectionUiSchema,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Normal collection — has fields (required) and optional indexes
|
|
63
|
+
export const NormalCollectionConfigSchema = z.object({
|
|
64
|
+
virtual: z.literal(false).optional(),
|
|
46
65
|
category: z.string().optional(),
|
|
47
66
|
singleton: z.boolean().optional(),
|
|
48
67
|
hooks: CollectionHooksSchema,
|
|
49
|
-
indexes: CollectionIndexesSchema,
|
|
68
|
+
indexes: CollectionIndexesSchema.optional(),
|
|
50
69
|
fields: CollectionFieldsSchema,
|
|
70
|
+
ui: CollectionUiSchema,
|
|
51
71
|
});
|
|
52
72
|
|
|
73
|
+
export const CollectionConfigSchema = z.union([
|
|
74
|
+
VirtualCollectionConfigSchema,
|
|
75
|
+
NormalCollectionConfigSchema,
|
|
76
|
+
]);
|
|
77
|
+
|
|
53
78
|
export const CollectionsConfigSchema = z.record(CollectionConfigSchema);
|
|
54
79
|
|
|
55
80
|
// Inferred Types
|
|
@@ -59,5 +84,7 @@ export type CollectionIndex = z.infer<typeof CollectionIndexSchema>;
|
|
|
59
84
|
export type CollectionIndexes = z.infer<typeof CollectionIndexesSchema>;
|
|
60
85
|
export type CollectionFields = z.infer<typeof CollectionFieldsSchema>;
|
|
61
86
|
export type CollectionFieldsWithoutId = Omit<CollectionFields, "id">;
|
|
87
|
+
export type VirtualCollectionConfig = z.infer<typeof VirtualCollectionConfigSchema>;
|
|
88
|
+
export type NormalCollectionConfig = z.infer<typeof NormalCollectionConfigSchema>;
|
|
62
89
|
export type CollectionConfig = z.infer<typeof CollectionConfigSchema>;
|
|
63
90
|
export type CollectionsConfig = z.infer<typeof CollectionsConfigSchema>;
|
package/src/types/index.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type { Workflow } from "../../WorkflowSystem.ts";
|
|
|
2
2
|
import { Lobb } from "../../../Lobb.ts";
|
|
3
3
|
|
|
4
4
|
async function applyDefaults(input: any) {
|
|
5
|
-
|
|
5
|
+
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
|
|
6
|
+
const fields = Lobb.instance.configManager.getNormalCollection(input.collectionName).fields;
|
|
6
7
|
const data = input.data;
|
|
7
8
|
|
|
8
9
|
for (const [fieldName, fieldConfig] of Object.entries(fields) as [string, any][]) {
|
|
@@ -3,7 +3,8 @@ import { Lobb } from "../../../Lobb.ts";
|
|
|
3
3
|
import { LobbError } from "../../../LobbError.ts";
|
|
4
4
|
|
|
5
5
|
async function runEnumCheck(input: any, processAllFields: boolean) {
|
|
6
|
-
|
|
6
|
+
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
|
|
7
|
+
const fields = Lobb.instance.configManager.getNormalCollection(input.collectionName).fields;
|
|
7
8
|
const data = input.data;
|
|
8
9
|
const errors: Record<string, string[]> = {};
|
|
9
10
|
|
|
@@ -5,9 +5,10 @@ async function runFieldHooks(
|
|
|
5
5
|
hookName: "beforeCreate" | "beforeUpdate" | "afterCreate" | "afterUpdate",
|
|
6
6
|
input: any,
|
|
7
7
|
) {
|
|
8
|
-
|
|
8
|
+
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return;
|
|
9
|
+
const fields = Lobb.instance.configManager.getNormalCollection(input.collectionName).fields;
|
|
9
10
|
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
|
10
|
-
const hook = fieldConfig.hooks?.[hookName];
|
|
11
|
+
const hook = "hooks" in fieldConfig ? fieldConfig.hooks?.[hookName] : undefined;
|
|
11
12
|
if (!hook) continue;
|
|
12
13
|
const result = await hook({ data: input.data, context: input.context });
|
|
13
14
|
if (result !== undefined) {
|
|
@@ -21,7 +22,8 @@ export const hooksWorkflows: Workflow[] = [
|
|
|
21
22
|
name: "core_hooksBeforeCreate",
|
|
22
23
|
eventName: "core.store.preCreateOne",
|
|
23
24
|
handler: async (input) => {
|
|
24
|
-
|
|
25
|
+
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
|
|
26
|
+
const hooks = Lobb.instance.configManager.getNormalCollection(input.collectionName).hooks;
|
|
25
27
|
await hooks?.beforeCreate?.({ data: input.data, context: input.context });
|
|
26
28
|
await runFieldHooks("beforeCreate", input);
|
|
27
29
|
return input;
|
|
@@ -31,7 +33,8 @@ export const hooksWorkflows: Workflow[] = [
|
|
|
31
33
|
name: "core_hooksBeforeUpdate",
|
|
32
34
|
eventName: "core.store.preUpdateOne",
|
|
33
35
|
handler: async (input) => {
|
|
34
|
-
|
|
36
|
+
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
|
|
37
|
+
const hooks = Lobb.instance.configManager.getNormalCollection(input.collectionName).hooks;
|
|
35
38
|
await hooks?.beforeUpdate?.({ data: input.data, context: input.context });
|
|
36
39
|
await runFieldHooks("beforeUpdate", input);
|
|
37
40
|
return input;
|
|
@@ -41,7 +44,8 @@ export const hooksWorkflows: Workflow[] = [
|
|
|
41
44
|
name: "core_hooksAfterCreate",
|
|
42
45
|
eventName: "core.store.createOne",
|
|
43
46
|
handler: async (input) => {
|
|
44
|
-
|
|
47
|
+
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
|
|
48
|
+
const hooks = Lobb.instance.configManager.getNormalCollection(input.collectionName).hooks;
|
|
45
49
|
await hooks?.afterCreate?.({ data: input.data, context: input.context });
|
|
46
50
|
await runFieldHooks("afterCreate", input);
|
|
47
51
|
return input;
|
|
@@ -51,7 +55,8 @@ export const hooksWorkflows: Workflow[] = [
|
|
|
51
55
|
name: "core_hooksAfterUpdate",
|
|
52
56
|
eventName: "core.store.updateOne",
|
|
53
57
|
handler: async (input) => {
|
|
54
|
-
|
|
58
|
+
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
|
|
59
|
+
const hooks = Lobb.instance.configManager.getNormalCollection(input.collectionName).hooks;
|
|
55
60
|
await hooks?.afterUpdate?.({ data: input.data, context: input.context });
|
|
56
61
|
await runFieldHooks("afterUpdate", input);
|
|
57
62
|
return input;
|
|
@@ -9,7 +9,7 @@ export async function processor(
|
|
|
9
9
|
processAllFields: boolean,
|
|
10
10
|
) {
|
|
11
11
|
const fieldsSchema =
|
|
12
|
-
Lobb.instance.configManager.
|
|
12
|
+
Lobb.instance.configManager.isCollectionVirtual(input.collectionName) ? {} as any : Lobb.instance.configManager.getNormalCollection(input.collectionName).fields;
|
|
13
13
|
|
|
14
14
|
input.data = await processPayloadWithSchema(
|
|
15
15
|
type,
|
|
@@ -3,7 +3,8 @@ import { Lobb } from "../../../Lobb.ts";
|
|
|
3
3
|
import { LobbError } from "../../../LobbError.ts";
|
|
4
4
|
|
|
5
5
|
async function runRequiredCheck(input: any, processAllFields: boolean) {
|
|
6
|
-
|
|
6
|
+
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
|
|
7
|
+
const fields = Lobb.instance.configManager.getNormalCollection(input.collectionName).fields;
|
|
7
8
|
const data = input.data;
|
|
8
9
|
const errors: Record<string, string[]> = {};
|
|
9
10
|
|
|
@@ -11,7 +12,7 @@ async function runRequiredCheck(input: any, processAllFields: boolean) {
|
|
|
11
12
|
|
|
12
13
|
for (const fieldName of fieldNames) {
|
|
13
14
|
const fieldConfig = fields[fieldName];
|
|
14
|
-
if (!fieldConfig
|
|
15
|
+
if (!fieldConfig || !("required" in fieldConfig) || !fieldConfig.required) continue;
|
|
15
16
|
|
|
16
17
|
const value = data[fieldName];
|
|
17
18
|
if (value === undefined || value === null || value === "") {
|
|
@@ -6,7 +6,7 @@ export async function validator(
|
|
|
6
6
|
processAllFields: boolean = false,
|
|
7
7
|
) {
|
|
8
8
|
const fieldsSchema =
|
|
9
|
-
Lobb.instance.configManager.
|
|
9
|
+
Lobb.instance.configManager.isCollectionVirtual(input.collectionName) ? {} as any : Lobb.instance.configManager.getNormalCollection(input.collectionName).fields;
|
|
10
10
|
|
|
11
11
|
await validatePayloadWithSchema(
|
|
12
12
|
input.data,
|
|
@@ -3,7 +3,8 @@ import { Lobb } from "../../../Lobb.ts";
|
|
|
3
3
|
import { LobbError } from "../../../LobbError.ts";
|
|
4
4
|
|
|
5
5
|
async function runFieldValidators(input: any, processAllFields: boolean) {
|
|
6
|
-
|
|
6
|
+
if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
|
|
7
|
+
const fields = Lobb.instance.configManager.getNormalCollection(input.collectionName).fields;
|
|
7
8
|
const data = input.data;
|
|
8
9
|
const errors: Record<string, string[]> = {};
|
|
9
10
|
|
|
@@ -11,7 +12,7 @@ async function runFieldValidators(input: any, processAllFields: boolean) {
|
|
|
11
12
|
|
|
12
13
|
for (const fieldName of fieldNames) {
|
|
13
14
|
const fieldConfig = fields[fieldName];
|
|
14
|
-
if (!fieldConfig
|
|
15
|
+
if (!fieldConfig || !("validator" in fieldConfig) || !fieldConfig.validator) continue;
|
|
15
16
|
|
|
16
17
|
const error = await fieldConfig.validator({
|
|
17
18
|
value: data[fieldName],
|