@lobb-js/core 0.18.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
3
  "license": "UNLICENSED",
4
- "version": "0.18.0",
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,
@@ -131,19 +139,30 @@ export class ConfigManager {
131
139
  return collection;
132
140
  }
133
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
+
134
153
  public getFieldNames(collectionName: string) {
135
- return Object.keys(
136
- this.getCollection(collectionName).fields,
137
- );
154
+ const collection = this.getCollection(collectionName);
155
+ if (collection.virtual) return [];
156
+ return Object.keys(collection.fields);
138
157
  }
139
158
 
140
159
  public fieldExists(
141
160
  fieldName: string,
142
161
  collectionName: string,
143
162
  ): boolean {
144
- return Boolean(
145
- this.getCollection(collectionName).fields[fieldName],
146
- );
163
+ const collection = this.getCollection(collectionName);
164
+ if (collection.virtual) return false;
165
+ return Boolean(collection.fields[fieldName]);
147
166
  }
148
167
 
149
168
  public collectionExists(collectionName: string): boolean {
@@ -154,7 +173,8 @@ export class ConfigManager {
154
173
  fieldName: string,
155
174
  collectionName: string,
156
175
  ): CollectionField {
157
- const field = this.getCollection(collectionName).fields[fieldName];
176
+ const collection = this.getCollection(collectionName);
177
+ const field = collection.virtual ? undefined : collection.fields[fieldName];
158
178
  if (!field) {
159
179
  throw new LobbError({
160
180
  code: "BAD_REQUEST",
@@ -205,7 +225,11 @@ export class ConfigManager {
205
225
  const virtualCollections: CollectionsConfig = {};
206
226
  const collections = this.getCollections();
207
227
  for (const [collectionName, collection] of Object.entries(collections)) {
208
- const collectionFieldsNames = Object.keys(collection.fields);
228
+ if (collection.virtual) {
229
+ virtualCollections[collectionName] = collection;
230
+ continue;
231
+ }
232
+ const collectionFieldsNames = Object.keys(collection.fields ?? {});
209
233
  if (collectionFieldsNames.length <= 1) {
210
234
  virtualCollections[collectionName] = collection;
211
235
  }
@@ -253,6 +277,12 @@ export class ConfigManager {
253
277
  }
254
278
 
255
279
  public isCollectionSingleton(collectionName: string): boolean {
256
- 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);
257
287
  }
258
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(
@@ -114,7 +114,7 @@ export class DatabaseSyncManager {
114
114
  } else if (change.path[1] === "fields" && change.op === "replace") {
115
115
  const collectionName = change.path[0] as string;
116
116
  const fieldName = change.path[2] as string;
117
- const fieldConfig = Lobb.instance.configManager.getCollection(collectionName).fields[fieldName];
117
+ const fieldConfig = Lobb.instance.configManager.getNormalCollection(collectionName).fields[fieldName];
118
118
  await this.dbDriver.alterField(collectionName, fieldName, fieldConfig);
119
119
  } else if (change.path[1] === "indexes" && change.op === "add") {
120
120
  const collectionName = change.path[0] as string;
@@ -131,10 +131,10 @@ export class DatabaseSyncManager {
131
131
  } else if (change.path[1] === "indexes" && change.op === "replace") {
132
132
  const collectionName = change.path[0] as string;
133
133
  const indexName = change.path[2] as string;
134
- const collectionConfig = Lobb.instance.configManager.getCollection(
134
+ const collectionConfig = Lobb.instance.configManager.getNormalCollection(
135
135
  collectionName,
136
136
  );
137
- const indexes = collectionConfig.indexes;
137
+ const indexes = collectionConfig.indexes ?? {};
138
138
  const index = indexes[indexName];
139
139
  await this.dbDriver.dropIndex(collectionName, indexName);
140
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
  ) {
@@ -423,7 +423,9 @@ export class PGDriver extends DatabaseDriver {
423
423
  }
424
424
 
425
425
  private stripVirtualFields(collectionName: string, data: any): any {
426
- const fields = Lobb.instance.configManager.getCollection(collectionName).fields;
426
+ const collection = Lobb.instance.configManager.getCollection(collectionName);
427
+ if (collection.virtual) return data;
428
+ const fields = collection.fields;
427
429
  const result = { ...data };
428
430
  for (const key of Object.keys(result)) {
429
431
  if (fields[key]?.virtual) delete result[key];
@@ -82,7 +82,7 @@ export class QueryBuilder {
82
82
  const mainFields = fields.filter((f) => !f.includes("."));
83
83
  mainFields.forEach((field) => {
84
84
  if (field === "*") {
85
- const collectionFields = Lobb.instance.configManager.getCollection(this.collectionName).fields;
85
+ const collectionFields = Lobb.instance.configManager.getNormalCollection(this.collectionName).fields;
86
86
  const currentCollectionFields = Object.entries(collectionFields)
87
87
  .filter(([, fieldConfig]) => !fieldConfig.virtual)
88
88
  .map(([fieldName]) => fieldName);
@@ -130,7 +130,7 @@ export class QueryBuilder {
130
130
  : foreignFieldCollection;
131
131
 
132
132
  // Exclude virtual fields — they have no DB column in the related table
133
- const foreignCollectionFields = Lobb.instance.configManager.getCollection(foreignFieldCollection).fields;
133
+ const foreignCollectionFields = Lobb.instance.configManager.getNormalCollection(foreignFieldCollection).fields;
134
134
  const nonVirtualNestedFields = nestedFields.filter((f) => !foreignCollectionFields[f]?.virtual);
135
135
 
136
136
  // Build JSON safely: only if related row exists
@@ -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],