@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lobb-js/core",
3
- "version": "0.17.0",
4
- "license": "AGPL-3.0-only",
3
+ "license": "UNLICENSED",
4
+ "version": "0.19.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -226,11 +226,15 @@ export class CollectionStore {
226
226
  transaction: client,
227
227
  },
228
228
  );
229
- data = await this.dbDriver.createOne(
230
- collectionName,
231
- this.filterPayload(collectionName, result.data),
232
- client,
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
  }
@@ -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
- return Object.keys(
130
- this.getCollection(collectionName).fields,
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
- return Boolean(
139
- this.getCollection(collectionName).fields[fieldName],
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 field = this.getCollection(collectionName).fields[fieldName];
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
- const collectionFieldsNames = Object.keys(collection.fields);
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
- return Boolean(this.config.collections[collectionName]?.singleton);
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.getCollection(
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
- CollectionConfig,
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: 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 currentCollectionFields = Object.keys(
86
- Lobb.instance.configManager.getCollection(this.collectionName).fields,
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
- ${nestedFields.map((f) => `'${f}', ${alias}.${f}`).join(", ")}
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
- // Specialized field schemas
61
- export const CollectionBoolFieldSchema = CollectionFieldBaseSchema.extend({
62
- type: z.literal("bool"),
63
- 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(),
64
66
  });
65
67
 
66
- export const CollectionDateFieldSchema = CollectionFieldBaseSchema.extend({
67
- type: z.literal("date"),
68
- default: z.union([z.string(), z.date()]).optional(),
69
- enum: z.array(z.union([z.string(), z.date()])).optional(),
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 CollectionDateTimeFieldSchema = CollectionFieldBaseSchema.extend({
73
- type: z.literal("datetime"),
74
- default: z.union([z.string(), z.date()]).optional(),
75
- enum: z.array(z.union([z.string(), z.date()])).optional(),
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 CollectionDecimalFieldSchema = CollectionFieldBaseSchema.extend({
79
- type: z.literal("decimal"),
80
- default: z.number().optional(),
81
- enum: z.array(z.number()).optional(),
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 CollectionFloatFieldSchema = CollectionFieldBaseSchema.extend({
85
- type: z.literal("float"),
86
- default: z.number().optional(),
87
- enum: z.array(z.number()).optional(),
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 CollectionIntegerFieldSchema = CollectionFieldBaseSchema.extend({
91
- type: z.literal("integer"),
92
- default: z.number().int().optional(),
93
- enum: z.array(z.number().int()).optional(),
94
- references: RelationCollectionFieldSchema.optional(),
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 CollectionLongFieldSchema = CollectionFieldBaseSchema.extend({
98
- type: z.literal("long"),
99
- default: z.number().optional(),
100
- enum: z.array(z.number()).optional(),
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 CollectionStringFieldSchema = CollectionFieldBaseSchema.extend({
104
- type: z.literal("string"),
105
- length: z.number(),
106
- default: z.string().optional(),
107
- enum: StringEnumSchema.optional(),
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 CollectionTextFieldSchema = CollectionFieldBaseSchema.extend({
111
- type: z.literal("text"),
112
- default: z.string().optional(),
113
- enum: StringEnumSchema.optional(),
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 CollectionTimeFieldSchema = CollectionFieldBaseSchema.extend({
117
- type: z.literal("time"),
118
- default: z.string().optional(),
119
- enum: StringEnumSchema.optional(),
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
- // CollectionConfig
45
- export const CollectionConfigSchema = z.object({
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>;
@@ -5,6 +5,7 @@ export type { Config, WebConfig } from "./config/config.ts";
5
5
  export type { RelationsConfig } from "./config/relations.ts";
6
6
  export type {
7
7
  CollectionConfig,
8
+ NormalCollectionConfig,
8
9
  CollectionIndex,
9
10
  CollectionIndexes,
10
11
  CollectionsConfig,
@@ -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
- const fields = Lobb.instance.configManager.getCollection(input.collectionName).fields;
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
- const fields = Lobb.instance.configManager.getCollection(input.collectionName).fields;
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
- const fields = Lobb.instance.configManager.getCollection(input.collectionName).fields;
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
- const hooks = Lobb.instance.configManager.getCollection(input.collectionName).hooks;
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
- const hooks = Lobb.instance.configManager.getCollection(input.collectionName).hooks;
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
- const hooks = Lobb.instance.configManager.getCollection(input.collectionName).hooks;
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
- const hooks = Lobb.instance.configManager.getCollection(input.collectionName).hooks;
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.getCollection(input.collectionName).fields;
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
- const fields = Lobb.instance.configManager.getCollection(input.collectionName).fields;
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?.required) continue;
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.getCollection(input.collectionName).fields;
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
- const fields = Lobb.instance.configManager.getCollection(input.collectionName).fields;
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?.validator) continue;
15
+ if (!fieldConfig || !("validator" in fieldConfig) || !fieldConfig.validator) continue;
15
16
 
16
17
  const error = await fieldConfig.validator({
17
18
  value: data[fieldName],