@lobb-js/core 0.17.0 → 0.18.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/config/ConfigManager.ts +6 -0
- package/src/database/DatabaseSyncManager.ts +5 -0
- package/src/database/drivers/pgDriver/PGDriver.ts +23 -0
- 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/package.json
CHANGED
|
@@ -89,6 +89,12 @@ export class ConfigManager {
|
|
|
89
89
|
};
|
|
90
90
|
const fields = collections[collectionName].fields;
|
|
91
91
|
for (const fieldName in fields) {
|
|
92
|
+
// virtual fields have no database column — skip them entirely
|
|
93
|
+
if (fields[fieldName].virtual) {
|
|
94
|
+
delete fields[fieldName];
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
92
98
|
// including only the properties that are db related
|
|
93
99
|
const field: any = {};
|
|
94
100
|
field["type"] = fields[fieldName].type;
|
|
@@ -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.getCollection(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;
|
|
@@ -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,22 @@ 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 fields = Lobb.instance.configManager.getCollection(collectionName).fields;
|
|
427
|
+
const result = { ...data };
|
|
428
|
+
for (const key of Object.keys(result)) {
|
|
429
|
+
if (fields[key]?.virtual) delete result[key];
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
433
|
+
|
|
412
434
|
public async updateOne(
|
|
413
435
|
collectionName: string,
|
|
414
436
|
id: string,
|
|
415
437
|
data: any,
|
|
416
438
|
localClient?: PoolClient,
|
|
417
439
|
) {
|
|
440
|
+
data = this.stripVirtualFields(collectionName, data);
|
|
418
441
|
const setClause = Object.keys(data).map((key) =>
|
|
419
442
|
format("%I = %L", key, data[key])
|
|
420
443
|
).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.getCollection(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.getCollection(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([
|