@lobb-js/core 0.16.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lobb-js/core",
3
- "version": "0.16.0",
4
- "license": "AGPL-3.0-only",
3
+ "license": "UNLICENSED",
4
+ "version": "0.18.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -63,6 +63,7 @@ export class MetaService {
63
63
  field.type = fieldConfig.type;
64
64
  field.label = fieldName;
65
65
  field.key = fieldName;
66
+ field.enum = "enum" in fieldConfig ? fieldConfig.enum : undefined;
66
67
  field.validators = fieldConfig.validators;
67
68
  field.pre_processors = fieldConfig.pre_processors;
68
69
  field.ui = fieldConfig.ui;
@@ -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 currentCollectionFields = Object.keys(
86
- Lobb.instance.configManager.getCollection(this.collectionName).fields,
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
- ${nestedFields.map((f) => `'${f}', ${alias}.${f}`).join(", ")}
141
+ ${nonVirtualNestedFields.map((f) => `'${f}', ${alias}.${f}`).join(", ")}
137
142
  )
138
143
  END AS ${foreignField}
139
144
  `;
package/src/index.ts CHANGED
@@ -28,6 +28,8 @@ export type { CollectionFieldsWithoutId } from "./types/config/collectionsConfig
28
28
  export type {
29
29
  CollectionField,
30
30
  CollectionFieldBase,
31
+ EnumOption,
32
+ EnumLevel,
31
33
  } from "./types/config/collectionFields.ts";
32
34
  export type { Dashboard, Extension } from "./types/Extension.ts";
33
35
  export { Field } from "./types/Field.ts";
@@ -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,
@@ -2,6 +2,17 @@ import { RelationCollectionFieldSchema } from "./relations.ts";
2
2
 
3
3
  import { z } from "zod";
4
4
 
5
+ export type EnumLevel = "success" | "warning" | "danger" | "info" | "neutral" | "muted";
6
+
7
+ export const EnumOptionSchema = z.object({
8
+ value: z.string(),
9
+ level: z.enum(["success", "warning", "danger", "info", "neutral", "muted"]),
10
+ });
11
+
12
+ export type EnumOption = z.infer<typeof EnumOptionSchema>;
13
+
14
+ const StringEnumSchema = z.union([z.array(z.string()), z.array(EnumOptionSchema)]);
15
+
5
16
  // Define the UiInput schema
6
17
  const UiInputSchema = z.object({
7
18
  type: z.string(),
@@ -35,6 +46,7 @@ const FieldHooksSchema = z.object({
35
46
 
36
47
  // Base field schema (no default — each specialized schema adds it with the correct type)
37
48
  export const CollectionFieldBaseSchema = z.object({
49
+ virtual: z.never().optional(),
38
50
  required: z.boolean().optional(),
39
51
  hooks: FieldHooksSchema,
40
52
  validator: z.custom<FieldValidatorFn>((val) => typeof val === "function").optional(),
@@ -46,67 +58,107 @@ export const CollectionFieldBaseSchema = z.object({
46
58
 
47
59
  export type CollectionFieldBase = z.infer<typeof CollectionFieldBaseSchema>;
48
60
 
49
- // Specialized field schemas
50
- export const CollectionBoolFieldSchema = CollectionFieldBaseSchema.extend({
51
- type: z.literal("bool"),
52
- default: z.boolean().optional(),
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(),
53
66
  });
54
67
 
55
- export const CollectionDateFieldSchema = CollectionFieldBaseSchema.extend({
56
- type: z.literal("date"),
57
- default: z.union([z.string(), z.date()]).optional(),
58
- enum: z.array(z.union([z.string(), z.date()])).optional(),
59
- });
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
+ ]);
60
76
 
61
- export const CollectionDateTimeFieldSchema = CollectionFieldBaseSchema.extend({
62
- type: z.literal("datetime"),
63
- default: z.union([z.string(), z.date()]).optional(),
64
- enum: z.array(z.union([z.string(), z.date()])).optional(),
65
- });
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
+ ]);
66
85
 
67
- export const CollectionDecimalFieldSchema = CollectionFieldBaseSchema.extend({
68
- type: z.literal("decimal"),
69
- default: z.number().optional(),
70
- enum: z.array(z.number()).optional(),
71
- });
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
+ ]);
72
94
 
73
- export const CollectionFloatFieldSchema = CollectionFieldBaseSchema.extend({
74
- type: z.literal("float"),
75
- default: z.number().optional(),
76
- enum: z.array(z.number()).optional(),
77
- });
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
+ ]);
78
103
 
79
- export const CollectionIntegerFieldSchema = CollectionFieldBaseSchema.extend({
80
- type: z.literal("integer"),
81
- default: z.number().int().optional(),
82
- enum: z.array(z.number().int()).optional(),
83
- references: RelationCollectionFieldSchema.optional(),
84
- });
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
+ ]);
85
112
 
86
- export const CollectionLongFieldSchema = CollectionFieldBaseSchema.extend({
87
- type: z.literal("long"),
88
- default: z.number().optional(),
89
- enum: z.array(z.number()).optional(),
90
- });
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
+ ]);
91
122
 
92
- export const CollectionStringFieldSchema = CollectionFieldBaseSchema.extend({
93
- type: z.literal("string"),
94
- length: z.number(),
95
- default: z.string().optional(),
96
- enum: z.array(z.string()).optional(),
97
- });
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
+ ]);
98
131
 
99
- export const CollectionTextFieldSchema = CollectionFieldBaseSchema.extend({
100
- type: z.literal("text"),
101
- default: z.string().optional(),
102
- enum: z.array(z.string()).optional(),
103
- });
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
+ ]);
104
144
 
105
- export const CollectionTimeFieldSchema = CollectionFieldBaseSchema.extend({
106
- type: z.literal("time"),
107
- default: z.string().optional(),
108
- enum: z.array(z.string()).optional(),
109
- });
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
+ ]);
110
162
 
111
163
  // Union type if you want a single schema for all
112
164
  export const CollectionFieldSchema = z.union([
@@ -16,8 +16,11 @@ async function runEnumCheck(input: any, processAllFields: boolean) {
16
16
  const value = data[fieldName];
17
17
  if (value === undefined || value === null) continue;
18
18
 
19
- if (!fieldConfig.enum.includes(value)) {
20
- errors[fieldName] = [`${fieldName} must be one of: ${fieldConfig.enum.join(", ")}`];
19
+ const allowedValues = fieldConfig.enum.map((item: any) =>
20
+ typeof item === "object" ? item.value : item
21
+ );
22
+ if (!allowedValues.includes(value)) {
23
+ errors[fieldName] = [`${fieldName} must be one of: ${allowedValues.join(", ")}`];
21
24
  }
22
25
  }
23
26