@lobb-js/core 0.28.0 → 0.30.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.28.0",
4
+ "version": "0.30.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -37,6 +37,7 @@
37
37
  "zod-to-ts": "^1.2.0"
38
38
  },
39
39
  "devDependencies": {
40
+ "lucide-svelte": "latest",
40
41
  "@types/lodash": "^4.17.0",
41
42
  "@types/nunjucks": "^3.2.6",
42
43
  "@types/pg": "^8.11.0",
@@ -46,12 +46,15 @@ export const collectionControllers: CollectionControllers = {
46
46
  });
47
47
  }
48
48
 
49
+ const dryRun = c.req.query("dry_run") !== undefined;
50
+
49
51
  const result = await Lobb.instance.collectionService.createOne({
50
52
  collectionName,
51
53
  context: c,
52
54
  data: body.data,
53
55
  children: body.children,
54
56
  triggeredBy: "API",
57
+ dryRun,
55
58
  });
56
59
 
57
60
  return c.json(result, 201);
@@ -209,6 +212,8 @@ export const collectionControllers: CollectionControllers = {
209
212
  }
210
213
  }
211
214
 
215
+ const dryRun = c.req.query("dry_run") !== undefined;
216
+
212
217
  const result = await Lobb.instance.collectionService.updateOne({
213
218
  collectionName,
214
219
  context: c,
@@ -216,6 +221,7 @@ export const collectionControllers: CollectionControllers = {
216
221
  data: body.data,
217
222
  children: body.children,
218
223
  triggeredBy: "API",
224
+ dryRun,
219
225
  });
220
226
  return c.json(result, 200);
221
227
  },
@@ -245,12 +251,15 @@ export const collectionControllers: CollectionControllers = {
245
251
  }
246
252
  }
247
253
 
254
+ const dryRun = c.req.query("dry_run") !== undefined;
255
+
248
256
  const result = await Lobb.instance.collectionService.deleteOne({
249
257
  collectionName,
250
258
  context: c,
251
259
  id,
252
260
  force,
253
261
  triggeredBy: "API",
262
+ dryRun,
254
263
  });
255
264
 
256
265
  return c.json(result);
@@ -41,6 +41,16 @@ export class CollectionStore {
41
41
  context?: HonoContext;
42
42
  client?: PoolClient;
43
43
  }): Promise<ExposedManyServiceOutput> {
44
+ // Resolve children_of + parent_id into a regular filter before proceeding.
45
+ // This allows fetching related records (fk, m2m, polymorphic) without the caller
46
+ // needing to know the relation type or junction details.
47
+ if ((params as any)?.children_of !== undefined) {
48
+ const { children_of, parent_id, ...rest } = params as any;
49
+ const childFilter = await this.resolveChildrenOf(collectionName, children_of, parent_id);
50
+ if (childFilter === null) return { data: [], meta: { totalCount: 0 } };
51
+ params = { ...rest, filter: { ...rest.filter, ...childFilter } };
52
+ }
53
+
44
54
  return await beginTransaction(
45
55
  async (client) => {
46
56
  const eventResult = await Lobb.instance.eventSystem.emit(
@@ -224,6 +234,7 @@ export class CollectionStore {
224
234
  triggeredBy = "INTERNAL",
225
235
  context,
226
236
  client,
237
+ dryRun = false,
227
238
  }: {
228
239
  collectionName: string;
229
240
  data: Record<string, any>;
@@ -231,6 +242,7 @@ export class CollectionStore {
231
242
  triggeredBy?: TriggeredBy;
232
243
  context?: HonoContext;
233
244
  client?: PoolClient;
245
+ dryRun?: boolean;
234
246
  }): Promise<ExposedServiceOutput> {
235
247
  return await beginTransaction(
236
248
  async (client) => {
@@ -248,6 +260,7 @@ export class CollectionStore {
248
260
  context: context,
249
261
  triggeredBy: triggeredBy,
250
262
  transaction: client,
263
+ dryRun,
251
264
  },
252
265
  );
253
266
 
@@ -259,7 +272,6 @@ export class CollectionStore {
259
272
  );
260
273
  }
261
274
 
262
-
263
275
  data = (await Lobb.instance.eventSystem.emit(
264
276
  `core.store.createOne`,
265
277
  {
@@ -291,6 +303,7 @@ export class CollectionStore {
291
303
  triggeredBy = "INTERNAL",
292
304
  context,
293
305
  client,
306
+ dryRun = false,
294
307
  }: {
295
308
  collectionName: string;
296
309
  id: string;
@@ -299,6 +312,7 @@ export class CollectionStore {
299
312
  triggeredBy?: TriggeredBy;
300
313
  context?: HonoContext;
301
314
  client?: PoolClient;
315
+ dryRun?: boolean;
302
316
  }): Promise<ExposedServiceOutput> {
303
317
  return await beginTransaction(
304
318
  async (client) => {
@@ -311,8 +325,10 @@ export class CollectionStore {
311
325
  context: context,
312
326
  triggeredBy: triggeredBy,
313
327
  transaction: client,
328
+ dryRun,
314
329
  },
315
330
  );
331
+
316
332
  data = await this.dbDriver.updateOne(
317
333
  collectionName,
318
334
  result.id,
@@ -351,6 +367,7 @@ export class CollectionStore {
351
367
  triggeredBy = "INTERNAL",
352
368
  context,
353
369
  client,
370
+ dryRun = false,
354
371
  }: {
355
372
  collectionName: string;
356
373
  id: string;
@@ -358,6 +375,7 @@ export class CollectionStore {
358
375
  triggeredBy?: TriggeredBy;
359
376
  context?: HonoContext;
360
377
  client?: PoolClient;
378
+ dryRun?: boolean;
361
379
  },
362
380
  ): Promise<ExposedServiceOutput> {
363
381
  return await beginTransaction(
@@ -370,6 +388,7 @@ export class CollectionStore {
370
388
  context: context,
371
389
  triggeredBy: triggeredBy,
372
390
  transaction: client,
391
+ dryRun,
373
392
  },
374
393
  );
375
394
 
@@ -1222,6 +1241,62 @@ export class CollectionStore {
1222
1241
  }
1223
1242
  }
1224
1243
 
1244
+ private async resolveChildrenOf(
1245
+ collectionName: string,
1246
+ childrenOf: string,
1247
+ parentId: any,
1248
+ ): Promise<Record<string, any> | null> {
1249
+ const children = Lobb.instance.configManager.getCollectionChildren(childrenOf);
1250
+ const child = children.find((c) => c.collection === collectionName);
1251
+
1252
+ if (!child) {
1253
+ throw new LobbError({
1254
+ code: "BAD_REQUEST",
1255
+ message: `"${collectionName}" is not a child of "${childrenOf}"`,
1256
+ });
1257
+ }
1258
+
1259
+ if (child.type === "fk") {
1260
+ return { [child.field!]: parentId };
1261
+ }
1262
+
1263
+ if (child.type === "m2m") {
1264
+ const junction = await this.findAll({
1265
+ collectionName: child.junction!,
1266
+ params: { filter: { [child.junction_field!]: parentId } },
1267
+ });
1268
+ const ids = junction.data.map((r: any) => r[child.target_field!]);
1269
+ if (ids.length === 0) return null;
1270
+ return { id: { $in: ids } };
1271
+ }
1272
+
1273
+ if (child.type === "polymorphic") {
1274
+ const relations = Lobb.instance.configManager.config.relations ?? [];
1275
+ const polyRelation = relations.find(
1276
+ (r: any) =>
1277
+ r.type === "polymorphic" &&
1278
+ r.from.collection === collectionName &&
1279
+ Array.isArray(r.to) &&
1280
+ r.to.includes(childrenOf),
1281
+ ) as any;
1282
+ if (!polyRelation) {
1283
+ throw new LobbError({
1284
+ code: "BAD_REQUEST",
1285
+ message: `No polymorphic relation found between "${collectionName}" and "${childrenOf}"`,
1286
+ });
1287
+ }
1288
+ return {
1289
+ [polyRelation.from.collection_field]: childrenOf,
1290
+ [polyRelation.from.id_field]: parentId,
1291
+ };
1292
+ }
1293
+
1294
+ throw new LobbError({
1295
+ code: "BAD_REQUEST",
1296
+ message: `Unknown relation type: ${child.type}`,
1297
+ });
1298
+ }
1299
+
1225
1300
  private async handleSingletonCollection(
1226
1301
  collectionName: string,
1227
1302
  context?: HonoContext,
@@ -62,7 +62,7 @@ export class ConfigManager {
62
62
  } as any;
63
63
 
64
64
  // Convert the original field to virtual (drives the UI picker)
65
- collection.fields[fieldName] = { virtual: true, ui: { icon: "Shuffle" } } as any;
65
+ collection.fields[fieldName] = { virtual: true, ui: { icon: "GitFork" } } as any;
66
66
 
67
67
  // Auto-register the polymorphic relation
68
68
  if (!this.config.relations) this.config.relations = [];
@@ -425,10 +425,10 @@ export class ConfigManager {
425
425
  return [];
426
426
  }
427
427
 
428
- public getCollectionChildren(collectionName: string): { type: string; collection: string; field?: string }[] {
428
+ public getCollectionChildren(collectionName: string): { type: string; collection: string; field?: string; junction?: string; junction_field?: string; target_field?: string }[] {
429
429
  const allRelations = this.config.relations ?? [];
430
430
  const allCollections = this.config.collections;
431
- const children: { type: string; collection: string; field?: string }[] = [];
431
+ const children: { type: string; collection: string; field?: string; junction?: string; junction_field?: string; target_field?: string }[] = [];
432
432
  const seenJunctions = new Set<string>();
433
433
 
434
434
  for (const relation of allRelations) {
@@ -446,7 +446,13 @@ export class ConfigManager {
446
446
  (r as RegularRelation).from.field !== reg.from.field,
447
447
  ) as RegularRelation | undefined;
448
448
  if (otherSide) {
449
- children.push({ type: "m2m", collection: otherSide.to.collection });
449
+ children.push({
450
+ type: "m2m",
451
+ collection: otherSide.to.collection,
452
+ junction: childCollection,
453
+ junction_field: reg.from.field,
454
+ target_field: otherSide.from.field,
455
+ });
450
456
  }
451
457
  } else {
452
458
  children.push({ type: "fk", collection: childCollection, field: reg.from.field });
@@ -95,6 +95,7 @@ export class ExtensionSystem {
95
95
  ? await extension.meta(Lobb.instance)
96
96
  : {};
97
97
  extensionMeta.version = extension.version;
98
+ extensionMeta.icon = extension.icon;
98
99
  return extensionMeta;
99
100
  }
100
101
  }
@@ -5,6 +5,7 @@ import type { OpenAPIV3_1 } from "openapi-types";
5
5
  import type { RelationsConfig } from "./config/relations.ts";
6
6
  import type { Lobb } from "../Lobb.ts";
7
7
  import type { Workflow } from "../workflows/WorkflowSystem.ts";
8
+ import * as Icons from "lucide-svelte";
8
9
 
9
10
  interface OpenApiProperty {
10
11
  paths: OpenAPIV3_1.Document["paths"];
@@ -17,6 +18,7 @@ export interface Dashboard {
17
18
 
18
19
  interface ExtensionBase {
19
20
  name: string;
21
+ icon?: keyof typeof Icons;
20
22
  workflows?: Workflow[];
21
23
  migrations?: Migrations;
22
24
  init?: (lobb: Lobb) => Promise<void>;
@@ -9,6 +9,7 @@ import { enumWorkflows } from "./processors/enumWorkflows.ts";
9
9
  import { coerceAndValidateWorkflows } from "./processors/coerceAndValidateWorkflows.ts";
10
10
  import { coreWorkflowsCollectionWorkflows } from "./workflowsCollection/workflowsCollectionWorkflows.ts";
11
11
  import { getQueryCoreWorkflows } from "./queryCoreWorkflows.ts";
12
+ import { dryRunWorkflows } from "./processors/dryRunWorkflows.ts";
12
13
 
13
14
  export function getCoreWorkflows(): Array<Workflow> {
14
15
  return [
@@ -23,5 +24,6 @@ export function getCoreWorkflows(): Array<Workflow> {
23
24
  ...getCollectionsTableWorkflows(),
24
25
  ...getQueryCoreWorkflows(),
25
26
  ...getUtilsCoreWorkflows(),
27
+ ...dryRunWorkflows,
26
28
  ];
27
29
  }
@@ -0,0 +1,28 @@
1
+ import type { Workflow } from "../../WorkflowSystem.ts";
2
+
3
+ export const dryRunWorkflows: Workflow[] = [
4
+ {
5
+ name: "core_dryRunCreate",
6
+ eventName: "core.store.preCreateOne",
7
+ handler: async (input) => {
8
+ if (!input.dryRun) return input;
9
+ throw new Response(null, { status: 204 });
10
+ },
11
+ },
12
+ {
13
+ name: "core_dryRunUpdate",
14
+ eventName: "core.store.preUpdateOne",
15
+ handler: async (input) => {
16
+ if (!input.dryRun) return input;
17
+ throw new Response(null, { status: 204 });
18
+ },
19
+ },
20
+ {
21
+ name: "core_dryRunDelete",
22
+ eventName: "core.store.preDeleteOne",
23
+ handler: async (input) => {
24
+ if (!input.dryRun) return input;
25
+ throw new Response(null, { status: 204 });
26
+ },
27
+ },
28
+ ];