@jskit-ai/crud-core 0.1.30 → 0.1.31

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.
@@ -166,17 +166,263 @@ function resolveCrudRepositoryCall(runtime, knex, repositoryOptions = {}, callOp
166
166
  });
167
167
  }
168
168
 
169
- async function crudRepositoryList(runtime, knex, query = {}, repositoryOptions = {}, callOptions = {}) {
169
+ function normalizeCrudRepositoryHooks(hooks = null, allowedHookKeys = [], { context = "crudRepository" } = {}) {
170
+ if (hooks === null || hooks === undefined) {
171
+ return Object.freeze({});
172
+ }
173
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
174
+ throw new TypeError(`${context} hooks must be an object when provided.`);
175
+ }
176
+
177
+ const supportedHookKeys = (Array.isArray(allowedHookKeys) ? allowedHookKeys : [])
178
+ .map((entry) => normalizeText(entry))
179
+ .filter(Boolean);
180
+ const supportedHookKeySet = new Set(supportedHookKeys);
181
+ const normalizedHooks = {};
182
+
183
+ for (const [rawHookKey, rawHook] of Object.entries(hooks)) {
184
+ const normalizedHookKey = normalizeText(rawHookKey);
185
+ if (!normalizedHookKey) {
186
+ continue;
187
+ }
188
+ if (!supportedHookKeySet.has(normalizedHookKey)) {
189
+ throw new TypeError(
190
+ `${context} does not support hooks.${normalizedHookKey}. Allowed hooks: ${supportedHookKeys.join(", ")}.`
191
+ );
192
+ }
193
+
194
+ if (rawHook === null || rawHook === undefined) {
195
+ continue;
196
+ }
197
+ if (typeof rawHook !== "function") {
198
+ throw new TypeError(`${context} hooks.${normalizedHookKey} must be a function.`);
199
+ }
200
+
201
+ normalizedHooks[normalizedHookKey] = rawHook;
202
+ }
203
+
204
+ return Object.freeze(normalizedHooks);
205
+ }
206
+
207
+ function resolveOptionalCrudRepositoryHook(
208
+ hooks = {},
209
+ hookKey = "",
210
+ { context = "crudRepository" } = {}
211
+ ) {
212
+ const normalizedHookKey = normalizeText(hookKey);
213
+ if (!normalizedHookKey) {
214
+ return null;
215
+ }
216
+
217
+ const directHook = hooks?.[normalizedHookKey];
218
+ if (directHook !== undefined && directHook !== null) {
219
+ if (typeof directHook !== "function") {
220
+ throw new TypeError(`${context} hooks.${normalizedHookKey} must be a function.`);
221
+ }
222
+ return directHook;
223
+ }
224
+ return null;
225
+ }
226
+
227
+ async function applyCrudRepositoryQueryHook(
228
+ dbQuery,
229
+ hook = null,
230
+ hookContext = {},
231
+ { context = "crudRepository", hookKey = "modifyQuery" } = {}
232
+ ) {
233
+ if (typeof hook !== "function") {
234
+ return {
235
+ queryBuilder: dbQuery
236
+ };
237
+ }
238
+
239
+ const directResult = hook(dbQuery, hookContext);
240
+ if (directResult === undefined || directResult === dbQuery) {
241
+ return {
242
+ queryBuilder: dbQuery
243
+ };
244
+ }
245
+
246
+ if (directResult && typeof directResult.then === "function") {
247
+ const awaitedResult = await directResult;
248
+ if (awaitedResult === undefined || awaitedResult === dbQuery) {
249
+ return {
250
+ queryBuilder: dbQuery
251
+ };
252
+ }
253
+ }
254
+
255
+ throw new TypeError(
256
+ `${context} hooks.${hookKey} must mutate the provided query builder and return undefined or the same builder.`
257
+ );
258
+ }
259
+
260
+ function normalizeCrudRepositoryObjectInput(value = {}) {
261
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
262
+ return {};
263
+ }
264
+
265
+ return value;
266
+ }
267
+
268
+ async function applyCrudRepositoryPayloadHook(
269
+ payload = {},
270
+ hook = null,
271
+ hookContext = {},
272
+ { context = "crudRepository", hookKey = "modifyPayload" } = {}
273
+ ) {
274
+ const normalizedPayload = normalizeCrudRepositoryObjectInput(payload);
275
+ if (typeof hook !== "function") {
276
+ return normalizedPayload;
277
+ }
278
+
279
+ const nextPayload = await hook(normalizedPayload, hookContext);
280
+ if (nextPayload === undefined) {
281
+ return normalizedPayload;
282
+ }
283
+
284
+ if (!nextPayload || typeof nextPayload !== "object" || Array.isArray(nextPayload)) {
285
+ throw new TypeError(`${context} hooks.${hookKey} must return an object when it returns a value.`);
286
+ }
287
+
288
+ return nextPayload;
289
+ }
290
+
291
+ async function applyCrudRepositoryRecordsHook(
292
+ items = [],
293
+ hook = null,
294
+ hookContext = {},
295
+ { context = "crudRepository", hookKey = "afterQuery" } = {}
296
+ ) {
297
+ const normalizedItems = Array.isArray(items) ? items : [];
298
+ if (typeof hook !== "function") {
299
+ return normalizedItems;
300
+ }
301
+
302
+ const nextItems = await hook(normalizedItems, hookContext);
303
+ if (nextItems === undefined) {
304
+ return normalizedItems;
305
+ }
306
+ if (!Array.isArray(nextItems)) {
307
+ throw new TypeError(`${context} hooks.${hookKey} must return an array when it returns a value.`);
308
+ }
309
+
310
+ return nextItems;
311
+ }
312
+
313
+ async function applyCrudRepositoryRecordHook(
314
+ record = null,
315
+ hook = null,
316
+ hookContext = {},
317
+ { context = "crudRepository", hookKey = "transformReturnedRecord" } = {}
318
+ ) {
319
+ if (record === null || record === undefined) {
320
+ return null;
321
+ }
322
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
323
+ throw new TypeError(`${context} ${hookKey} record must be an object.`);
324
+ }
325
+ if (typeof hook !== "function") {
326
+ return record;
327
+ }
328
+
329
+ const nextRecord = await hook(record, hookContext);
330
+ if (nextRecord === undefined) {
331
+ return record;
332
+ }
333
+ if (!nextRecord || typeof nextRecord !== "object" || Array.isArray(nextRecord)) {
334
+ throw new TypeError(`${context} hooks.${hookKey} must return an object when it returns a value.`);
335
+ }
336
+
337
+ return nextRecord;
338
+ }
339
+
340
+ async function applyCrudRepositoryOutputHook(
341
+ output,
342
+ hook = null,
343
+ hookContext = {},
344
+ { context = "crudRepository", hookKey = "finalizeOutput", validateOutput = null } = {}
345
+ ) {
346
+ if (typeof hook !== "function") {
347
+ if (typeof validateOutput === "function") {
348
+ validateOutput(output);
349
+ }
350
+ return output;
351
+ }
352
+
353
+ const nextOutput = await hook(output, hookContext);
354
+ const resolvedOutput = nextOutput === undefined ? output : nextOutput;
355
+ if (typeof validateOutput === "function") {
356
+ validateOutput(resolvedOutput);
357
+ }
358
+
359
+ return resolvedOutput;
360
+ }
361
+
362
+ async function applyCrudRepositoryAfterWriteHook(
363
+ meta = {},
364
+ hook = null,
365
+ hookContext = {},
366
+ { context = "crudRepository", hookKey = "afterWrite" } = {}
367
+ ) {
368
+ if (typeof hook !== "function") {
369
+ return;
370
+ }
371
+
372
+ await hook(meta, hookContext);
373
+ }
374
+
375
+ function enforceCrudRepositoryListControls(dbQuery, { idColumn = "id", limit = DEFAULT_LIST_LIMIT + 1 } = {}) {
376
+ let nextQuery = dbQuery;
377
+ if (typeof nextQuery.clearOrder === "function") {
378
+ nextQuery = nextQuery.clearOrder();
379
+ }
380
+ if (typeof nextQuery.clear === "function") {
381
+ nextQuery = nextQuery.clear("limit");
382
+ }
383
+
384
+ return nextQuery
385
+ .orderBy(idColumn, "asc")
386
+ .limit(limit);
387
+ }
388
+
389
+ function createCrudRepositoryHookContextBase(runtime, repositoryOptions = {}, callOptions = {}) {
390
+ return {
391
+ runtime,
392
+ repositoryOptions,
393
+ callOptions,
394
+ state: {}
395
+ };
396
+ }
397
+
398
+ async function crudRepositoryList(runtime, knex, query = {}, repositoryOptions = {}, callOptions = {}, hooks = null) {
170
399
  const { client, tableName, idColumn, visible } = resolveCrudRepositoryCall(runtime, knex, repositoryOptions, callOptions);
400
+ const methodHooks = normalizeCrudRepositoryHooks(
401
+ hooks,
402
+ ["modifyQuery", "afterQuery", "transformReturnedRecord", "finalizeOutput"],
403
+ {
404
+ context: "crudRepositoryList"
405
+ }
406
+ );
407
+ const modifyListQuery = resolveOptionalCrudRepositoryHook(methodHooks, "modifyQuery", {
408
+ context: "crudRepositoryList"
409
+ });
410
+ const afterListQuery = resolveOptionalCrudRepositoryHook(methodHooks, "afterQuery", {
411
+ context: "crudRepositoryList"
412
+ });
413
+ const transformListRecord = resolveOptionalCrudRepositoryHook(methodHooks, "transformReturnedRecord", {
414
+ context: "crudRepositoryList"
415
+ });
416
+ const finalizeListOutput = resolveOptionalCrudRepositoryHook(methodHooks, "finalizeOutput", {
417
+ context: "crudRepositoryList"
418
+ });
171
419
  const normalizedLimit = normalizeCrudListLimit(query?.limit, {
172
420
  fallback: runtime.list.defaultLimit,
173
421
  max: runtime.list.maxLimit
174
422
  });
423
+ const hookContextBase = createCrudRepositoryHookContextBase(runtime, repositoryOptions, callOptions);
175
424
  let dbQuery = client(tableName)
176
- .select(...runtime.selectColumns)
177
- .where(visible)
178
- .orderBy(idColumn, "asc")
179
- .limit(normalizedLimit + 1);
425
+ .select(...runtime.selectColumns);
180
426
 
181
427
  dbQuery = applyCrudListQueryFilters(dbQuery, {
182
428
  idColumn,
@@ -187,6 +433,26 @@ async function crudRepositoryList(runtime, knex, query = {}, repositoryOptions =
187
433
  parentFilterColumns: runtime.mapping.parentFilterColumns
188
434
  });
189
435
 
436
+ const listHookResult = await applyCrudRepositoryQueryHook(
437
+ dbQuery,
438
+ modifyListQuery,
439
+ {
440
+ query,
441
+ ...hookContextBase
442
+ },
443
+ {
444
+ context: "crudRepositoryList",
445
+ hookKey: "modifyQuery"
446
+ }
447
+ );
448
+ dbQuery = listHookResult.queryBuilder;
449
+
450
+ dbQuery = dbQuery.where(visible);
451
+ dbQuery = enforceCrudRepositoryListControls(dbQuery, {
452
+ idColumn,
453
+ limit: normalizedLimit + 1
454
+ });
455
+
190
456
  const rows = await dbQuery;
191
457
  const hasMore = rows.length > normalizedLimit;
192
458
  const pageRows = hasMore ? rows.slice(0, normalizedLimit) : rows;
@@ -202,7 +468,7 @@ async function crudRepositoryList(runtime, knex, query = {}, repositoryOptions =
202
468
  }));
203
469
  }
204
470
 
205
- const hydratedItems = await hydrateCrudLookupRecords(items, {
471
+ let hydratedItems = await hydrateCrudLookupRecords(items, {
206
472
  ...runtime.lookup,
207
473
  context: runtime.context
208
474
  }, {
@@ -212,45 +478,211 @@ async function crudRepositoryList(runtime, knex, query = {}, repositoryOptions =
212
478
  callOptions
213
479
  });
214
480
 
215
- return {
216
- items: hydratedItems,
217
- nextCursor: hasMore && items.length > 0 ? String(items[items.length - 1].id) : null
481
+ hydratedItems = await applyCrudRepositoryRecordsHook(
482
+ hydratedItems,
483
+ afterListQuery,
484
+ {
485
+ query,
486
+ ...hookContextBase
487
+ },
488
+ {
489
+ context: "crudRepositoryList",
490
+ hookKey: "afterQuery"
491
+ }
492
+ );
493
+
494
+ const transformedItems = [];
495
+ for (let itemIndex = 0; itemIndex < hydratedItems.length; itemIndex += 1) {
496
+ transformedItems.push(
497
+ await applyCrudRepositoryRecordHook(
498
+ hydratedItems[itemIndex],
499
+ transformListRecord,
500
+ {
501
+ query,
502
+ itemIndex,
503
+ items: hydratedItems,
504
+ ...hookContextBase
505
+ },
506
+ {
507
+ context: "crudRepositoryList",
508
+ hookKey: "transformReturnedRecord"
509
+ }
510
+ )
511
+ );
512
+ }
513
+
514
+ const nextCursor = hasMore && hydratedItems.length > 0 ? String(hydratedItems[hydratedItems.length - 1].id) : null;
515
+ let output = {
516
+ items: transformedItems,
517
+ nextCursor
218
518
  };
519
+
520
+ output = await applyCrudRepositoryOutputHook(
521
+ output,
522
+ finalizeListOutput,
523
+ {
524
+ query,
525
+ ...hookContextBase
526
+ },
527
+ {
528
+ context: "crudRepositoryList",
529
+ hookKey: "finalizeOutput",
530
+ validateOutput(nextOutput) {
531
+ if (!nextOutput || typeof nextOutput !== "object" || Array.isArray(nextOutput)) {
532
+ throw new TypeError("crudRepositoryList hooks.finalizeOutput must return an object.");
533
+ }
534
+ if (!Object.hasOwn(nextOutput, "items")) {
535
+ throw new TypeError('crudRepositoryList hooks.finalizeOutput must return required key "items".');
536
+ }
537
+ if (!Object.hasOwn(nextOutput, "nextCursor")) {
538
+ throw new TypeError('crudRepositoryList hooks.finalizeOutput must return required key "nextCursor".');
539
+ }
540
+ if (!Array.isArray(nextOutput.items)) {
541
+ throw new TypeError('crudRepositoryList hooks.finalizeOutput must return "items" as an array.');
542
+ }
543
+ }
544
+ }
545
+ );
546
+
547
+ return output;
219
548
  }
220
549
 
221
- async function crudRepositoryFindById(runtime, knex, recordId, repositoryOptions = {}, callOptions = {}) {
550
+ async function crudRepositoryFindById(runtime, knex, recordId, repositoryOptions = {}, callOptions = {}, hooks = null) {
222
551
  const { client, tableName, idColumn, visible } = resolveCrudRepositoryCall(runtime, knex, repositoryOptions, callOptions);
223
- const row = await client(tableName)
224
- .select(...runtime.selectColumns)
552
+ const methodHooks = normalizeCrudRepositoryHooks(
553
+ hooks,
554
+ ["modifyQuery", "afterQuery", "transformReturnedRecord", "finalizeOutput"],
555
+ {
556
+ context: "crudRepositoryFindById"
557
+ }
558
+ );
559
+ const modifyFindByIdQuery = resolveOptionalCrudRepositoryHook(methodHooks, "modifyQuery", {
560
+ context: "crudRepositoryFindById"
561
+ });
562
+ const afterFindByIdQuery = resolveOptionalCrudRepositoryHook(methodHooks, "afterQuery", {
563
+ context: "crudRepositoryFindById"
564
+ });
565
+ const transformFindByIdRecord = resolveOptionalCrudRepositoryHook(methodHooks, "transformReturnedRecord", {
566
+ context: "crudRepositoryFindById"
567
+ });
568
+ const finalizeFindByIdOutput = resolveOptionalCrudRepositoryHook(methodHooks, "finalizeOutput", {
569
+ context: "crudRepositoryFindById"
570
+ });
571
+ const hookContextBase = createCrudRepositoryHookContextBase(runtime, repositoryOptions, callOptions);
572
+ let dbQuery = client(tableName)
573
+ .select(...runtime.selectColumns);
574
+
575
+ const findByIdHookResult = await applyCrudRepositoryQueryHook(
576
+ dbQuery,
577
+ modifyFindByIdQuery,
578
+ {
579
+ recordId,
580
+ ...hookContextBase
581
+ },
582
+ {
583
+ context: "crudRepositoryFindById",
584
+ hookKey: "modifyQuery"
585
+ }
586
+ );
587
+ dbQuery = findByIdHookResult.queryBuilder;
588
+
589
+ dbQuery = dbQuery
225
590
  .where(visible)
226
591
  .where({
227
592
  [idColumn]: Number(recordId)
228
- })
229
- .first();
593
+ });
594
+
595
+ const row = await dbQuery.first();
230
596
 
231
597
  const mappedRecord = mapRecordRow(row, runtime.mapping.outputKeys, runtime.mapping.columnOverrides);
232
- if (!mappedRecord) {
233
- return null;
598
+ let records = [];
599
+ if (mappedRecord) {
600
+ const normalizedRecord = await normalizeRepositoryOutputRecord(runtime, mappedRecord, {
601
+ operation: "findById"
602
+ });
603
+ const hydrated = await hydrateCrudLookupRecords([normalizedRecord], {
604
+ ...runtime.lookup,
605
+ context: runtime.context
606
+ }, {
607
+ include: callOptions?.include,
608
+ mode: "view",
609
+ repositoryOptions,
610
+ callOptions
611
+ });
612
+ records = hydrated[0] ? [hydrated[0]] : [];
234
613
  }
235
- const normalizedRecord = await normalizeRepositoryOutputRecord(runtime, mappedRecord, {
236
- operation: "findById"
237
- });
238
614
 
239
- const hydrated = await hydrateCrudLookupRecords([normalizedRecord], {
240
- ...runtime.lookup,
241
- context: runtime.context
242
- }, {
243
- include: callOptions?.include,
244
- mode: "view",
245
- repositoryOptions,
246
- callOptions
247
- });
615
+ records = await applyCrudRepositoryRecordsHook(
616
+ records,
617
+ afterFindByIdQuery,
618
+ {
619
+ recordId,
620
+ ...hookContextBase
621
+ },
622
+ {
623
+ context: "crudRepositoryFindById",
624
+ hookKey: "afterQuery"
625
+ }
626
+ );
248
627
 
249
- return hydrated[0] || null;
628
+ let output = records[0] || null;
629
+ output = await applyCrudRepositoryRecordHook(
630
+ output,
631
+ transformFindByIdRecord,
632
+ {
633
+ recordId,
634
+ ...hookContextBase
635
+ },
636
+ {
637
+ context: "crudRepositoryFindById",
638
+ hookKey: "transformReturnedRecord"
639
+ }
640
+ );
641
+
642
+ return applyCrudRepositoryOutputHook(
643
+ output,
644
+ finalizeFindByIdOutput,
645
+ {
646
+ recordId,
647
+ ...hookContextBase
648
+ },
649
+ {
650
+ context: "crudRepositoryFindById",
651
+ hookKey: "finalizeOutput",
652
+ validateOutput(nextOutput) {
653
+ if (nextOutput === null) {
654
+ return;
655
+ }
656
+ if (!nextOutput || typeof nextOutput !== "object" || Array.isArray(nextOutput)) {
657
+ throw new TypeError("crudRepositoryFindById hooks.finalizeOutput must return record object or null.");
658
+ }
659
+ }
660
+ }
661
+ );
250
662
  }
251
663
 
252
- async function crudRepositoryListByIds(runtime, knex, ids = [], repositoryOptions = {}, callOptions = {}) {
664
+ async function crudRepositoryListByIds(runtime, knex, ids = [], repositoryOptions = {}, callOptions = {}, hooks = null) {
253
665
  const { client, tableName, visible } = resolveCrudRepositoryCall(runtime, knex, repositoryOptions, callOptions);
666
+ const methodHooks = normalizeCrudRepositoryHooks(
667
+ hooks,
668
+ ["modifyQuery", "afterQuery", "transformReturnedRecord", "finalizeOutput"],
669
+ {
670
+ context: "crudRepositoryListByIds"
671
+ }
672
+ );
673
+ const modifyListByIdsQuery = resolveOptionalCrudRepositoryHook(methodHooks, "modifyQuery", {
674
+ context: "crudRepositoryListByIds"
675
+ });
676
+ const afterListByIdsQuery = resolveOptionalCrudRepositoryHook(methodHooks, "afterQuery", {
677
+ context: "crudRepositoryListByIds"
678
+ });
679
+ const transformListByIdsRecord = resolveOptionalCrudRepositoryHook(methodHooks, "transformReturnedRecord", {
680
+ context: "crudRepositoryListByIds"
681
+ });
682
+ const finalizeListByIdsOutput = resolveOptionalCrudRepositoryHook(methodHooks, "finalizeOutput", {
683
+ context: "crudRepositoryListByIds"
684
+ });
685
+ const hookContextBase = createCrudRepositoryHookContextBase(runtime, repositoryOptions, callOptions);
254
686
  const lookupValueKey = normalizeText(callOptions?.valueKey) || "id";
255
687
  if (!runtime.mapping.outputKeys.includes(lookupValueKey)) {
256
688
  throw new TypeError(
@@ -267,11 +699,31 @@ async function crudRepositoryListByIds(runtime, knex, ids = [], repositoryOption
267
699
  return [];
268
700
  }
269
701
 
270
- const rows = await client(tableName)
271
- .select(...runtime.selectColumns)
702
+ let dbQuery = client(tableName)
703
+ .select(...runtime.selectColumns);
704
+
705
+ const listByIdsHookResult = await applyCrudRepositoryQueryHook(
706
+ dbQuery,
707
+ modifyListByIdsQuery,
708
+ {
709
+ ids: normalizedIds,
710
+ lookupValueKey,
711
+ lookupColumn,
712
+ ...hookContextBase
713
+ },
714
+ {
715
+ context: "crudRepositoryListByIds",
716
+ hookKey: "modifyQuery"
717
+ }
718
+ );
719
+ dbQuery = listByIdsHookResult.queryBuilder;
720
+
721
+ dbQuery = dbQuery
272
722
  .where(visible)
273
723
  .whereIn(lookupColumn, normalizedIds);
274
724
 
725
+ const rows = await dbQuery;
726
+
275
727
  const records = [];
276
728
  for (const row of rows) {
277
729
  const mappedRecord = mapRecordRow(row, runtime.mapping.outputKeys, runtime.mapping.columnOverrides);
@@ -285,8 +737,7 @@ async function crudRepositoryListByIds(runtime, knex, ids = [], repositoryOption
285
737
  }
286
738
 
287
739
  const lookupInclude = callOptions?.include === undefined ? "none" : callOptions.include;
288
-
289
- return hydrateCrudLookupRecords(records, {
740
+ let hydratedRecords = await hydrateCrudLookupRecords(records, {
290
741
  ...runtime.lookup,
291
742
  context: runtime.context
292
743
  }, {
@@ -295,11 +746,116 @@ async function crudRepositoryListByIds(runtime, knex, ids = [], repositoryOption
295
746
  repositoryOptions,
296
747
  callOptions
297
748
  });
749
+
750
+ hydratedRecords = await applyCrudRepositoryRecordsHook(
751
+ hydratedRecords,
752
+ afterListByIdsQuery,
753
+ {
754
+ ids: normalizedIds,
755
+ ...hookContextBase
756
+ },
757
+ {
758
+ context: "crudRepositoryListByIds",
759
+ hookKey: "afterQuery"
760
+ }
761
+ );
762
+
763
+ const transformedRecords = [];
764
+ for (let itemIndex = 0; itemIndex < hydratedRecords.length; itemIndex += 1) {
765
+ transformedRecords.push(
766
+ await applyCrudRepositoryRecordHook(
767
+ hydratedRecords[itemIndex],
768
+ transformListByIdsRecord,
769
+ {
770
+ ids: normalizedIds,
771
+ itemIndex,
772
+ items: hydratedRecords,
773
+ ...hookContextBase
774
+ },
775
+ {
776
+ context: "crudRepositoryListByIds",
777
+ hookKey: "transformReturnedRecord"
778
+ }
779
+ )
780
+ );
781
+ }
782
+
783
+ return applyCrudRepositoryOutputHook(
784
+ transformedRecords,
785
+ finalizeListByIdsOutput,
786
+ {
787
+ ids: normalizedIds,
788
+ ...hookContextBase
789
+ },
790
+ {
791
+ context: "crudRepositoryListByIds",
792
+ hookKey: "finalizeOutput",
793
+ validateOutput(nextOutput) {
794
+ if (!Array.isArray(nextOutput)) {
795
+ throw new TypeError("crudRepositoryListByIds hooks.finalizeOutput must return an array.");
796
+ }
797
+ }
798
+ }
799
+ );
800
+ }
801
+
802
+ async function crudRepositoryListByForeignIds(
803
+ runtime,
804
+ knex,
805
+ ids = [],
806
+ foreignKey = "",
807
+ repositoryOptions = {},
808
+ callOptions = {},
809
+ hooks = null
810
+ ) {
811
+ const normalizedForeignKey = normalizeText(foreignKey);
812
+ if (!normalizedForeignKey) {
813
+ throw new TypeError(`${runtime?.context || "crudRepository"} listByForeignIds requires foreignKey.`);
814
+ }
815
+
816
+ return crudRepositoryListByIds(
817
+ runtime,
818
+ knex,
819
+ ids,
820
+ repositoryOptions,
821
+ {
822
+ ...callOptions,
823
+ valueKey: normalizedForeignKey
824
+ },
825
+ hooks
826
+ );
298
827
  }
299
828
 
300
- async function crudRepositoryCreate(runtime, knex, payload = {}, repositoryOptions = {}, callOptions = {}) {
829
+ async function crudRepositoryCreate(runtime, knex, payload = {}, repositoryOptions = {}, callOptions = {}, hooks = null) {
301
830
  const { client, tableName } = resolveCrudRepositoryCall(runtime, knex, repositoryOptions, callOptions);
302
- const insertPayload = buildWritePayload(payload, runtime.mapping.writeKeys, runtime.mapping.columnOverrides);
831
+ const methodHooks = normalizeCrudRepositoryHooks(hooks, ["modifyPayload", "modifyQuery", "afterWrite"], {
832
+ context: "crudRepositoryCreate"
833
+ });
834
+ const modifyCreatePayload = resolveOptionalCrudRepositoryHook(methodHooks, "modifyPayload", {
835
+ context: "crudRepositoryCreate"
836
+ });
837
+ const modifyCreateQuery = resolveOptionalCrudRepositoryHook(methodHooks, "modifyQuery", {
838
+ context: "crudRepositoryCreate"
839
+ });
840
+ const afterCreateWrite = resolveOptionalCrudRepositoryHook(methodHooks, "afterWrite", {
841
+ context: "crudRepositoryCreate"
842
+ });
843
+ const hookContextBase = createCrudRepositoryHookContextBase(runtime, repositoryOptions, callOptions);
844
+ let sourcePayload = normalizeCrudRepositoryObjectInput(payload);
845
+ sourcePayload = await applyCrudRepositoryPayloadHook(
846
+ sourcePayload,
847
+ modifyCreatePayload,
848
+ {
849
+ payload: sourcePayload,
850
+ ...hookContextBase
851
+ },
852
+ {
853
+ context: "crudRepositoryCreate",
854
+ hookKey: "modifyPayload"
855
+ }
856
+ );
857
+
858
+ const insertPayload = buildWritePayload(sourcePayload, runtime.mapping.writeKeys, runtime.mapping.columnOverrides);
303
859
  const timestamp = toInsertDateTime();
304
860
  if (runtime.defaults.createdAtColumn && !Object.hasOwn(insertPayload, runtime.defaults.createdAtColumn)) {
305
861
  insertPayload[runtime.defaults.createdAtColumn] = timestamp;
@@ -308,18 +864,83 @@ async function crudRepositoryCreate(runtime, knex, payload = {}, repositoryOptio
308
864
  insertPayload[runtime.defaults.updatedAtColumn] = timestamp;
309
865
  }
310
866
 
311
- const withOwners = applyVisibilityOwners(insertPayload, callOptions.visibilityContext);
312
- const [recordId] = await client(tableName).insert(withOwners);
867
+ let withOwners = applyVisibilityOwners(insertPayload, callOptions.visibilityContext);
868
+ let createQuery = client(tableName);
869
+ const createHookResult = await applyCrudRepositoryQueryHook(
870
+ createQuery,
871
+ modifyCreateQuery,
872
+ {
873
+ payload: {
874
+ ...withOwners
875
+ },
876
+ ...hookContextBase
877
+ },
878
+ {
879
+ context: "crudRepositoryCreate",
880
+ hookKey: "modifyQuery"
881
+ }
882
+ );
883
+ createQuery = createHookResult.queryBuilder;
884
+
885
+ const [recordId] = await createQuery.insert(withOwners);
313
886
 
314
- return crudRepositoryFindById(runtime, knex, recordId, repositoryOptions, {
887
+ const createdRecord = await crudRepositoryFindById(runtime, knex, recordId, repositoryOptions, {
315
888
  ...callOptions,
316
- trx: client
317
- });
889
+ trx: client,
890
+ sourceOperation: "create"
891
+ }, null);
892
+
893
+ await applyCrudRepositoryAfterWriteHook(
894
+ {
895
+ operation: "create",
896
+ recordId,
897
+ payload: {
898
+ ...withOwners
899
+ },
900
+ record: createdRecord,
901
+ output: createdRecord
902
+ },
903
+ afterCreateWrite,
904
+ hookContextBase,
905
+ {
906
+ context: "crudRepositoryCreate",
907
+ hookKey: "afterWrite"
908
+ }
909
+ );
910
+
911
+ return createdRecord;
318
912
  }
319
913
 
320
- async function crudRepositoryUpdateById(runtime, knex, recordId, patch = {}, repositoryOptions = {}, callOptions = {}) {
914
+ async function crudRepositoryUpdateById(runtime, knex, recordId, patch = {}, repositoryOptions = {}, callOptions = {}, hooks = null) {
321
915
  const { client, tableName, idColumn, visible } = resolveCrudRepositoryCall(runtime, knex, repositoryOptions, callOptions);
322
- const dbPatch = buildWritePayload(patch, runtime.mapping.writeKeys, runtime.mapping.columnOverrides);
916
+ const methodHooks = normalizeCrudRepositoryHooks(hooks, ["modifyPatch", "modifyQuery", "afterWrite"], {
917
+ context: "crudRepositoryUpdateById"
918
+ });
919
+ const modifyUpdatePatch = resolveOptionalCrudRepositoryHook(methodHooks, "modifyPatch", {
920
+ context: "crudRepositoryUpdateById"
921
+ });
922
+ const modifyUpdateByIdQuery = resolveOptionalCrudRepositoryHook(methodHooks, "modifyQuery", {
923
+ context: "crudRepositoryUpdateById"
924
+ });
925
+ const afterUpdateWrite = resolveOptionalCrudRepositoryHook(methodHooks, "afterWrite", {
926
+ context: "crudRepositoryUpdateById"
927
+ });
928
+ const hookContextBase = createCrudRepositoryHookContextBase(runtime, repositoryOptions, callOptions);
929
+ let sourcePatch = normalizeCrudRepositoryObjectInput(patch);
930
+ sourcePatch = await applyCrudRepositoryPayloadHook(
931
+ sourcePatch,
932
+ modifyUpdatePatch,
933
+ {
934
+ recordId,
935
+ patch: sourcePatch,
936
+ ...hookContextBase
937
+ },
938
+ {
939
+ context: "crudRepositoryUpdateById",
940
+ hookKey: "modifyPatch"
941
+ }
942
+ );
943
+ let dbPatch = buildWritePayload(sourcePatch, runtime.mapping.writeKeys, runtime.mapping.columnOverrides);
323
944
 
324
945
  if (runtime.defaults.updatedAtColumn) {
325
946
  dbPatch[runtime.defaults.updatedAtColumn] = toInsertDateTime();
@@ -328,46 +949,174 @@ async function crudRepositoryUpdateById(runtime, knex, recordId, patch = {}, rep
328
949
  if (Object.keys(dbPatch).length < 1) {
329
950
  return crudRepositoryFindById(runtime, knex, recordId, repositoryOptions, {
330
951
  ...callOptions,
331
- trx: client
332
- });
952
+ trx: client,
953
+ sourceOperation: "update"
954
+ }, null);
333
955
  }
334
956
 
335
- await client(tableName)
957
+ let updateQuery = client(tableName);
958
+ const updateHookResult = await applyCrudRepositoryQueryHook(
959
+ updateQuery,
960
+ modifyUpdateByIdQuery,
961
+ {
962
+ recordId,
963
+ patch: {
964
+ ...dbPatch
965
+ },
966
+ ...hookContextBase
967
+ },
968
+ {
969
+ context: "crudRepositoryUpdateById",
970
+ hookKey: "modifyQuery"
971
+ }
972
+ );
973
+ updateQuery = updateHookResult.queryBuilder;
974
+
975
+ updateQuery = updateQuery
336
976
  .where(visible)
337
977
  .where({
338
978
  [idColumn]: Number(recordId)
339
- })
340
- .update(dbPatch);
979
+ });
341
980
 
342
- return crudRepositoryFindById(runtime, knex, recordId, repositoryOptions, {
981
+ await updateQuery.update(dbPatch);
982
+
983
+ const updatedRecord = await crudRepositoryFindById(runtime, knex, recordId, repositoryOptions, {
343
984
  ...callOptions,
344
- trx: client
345
- });
985
+ trx: client,
986
+ sourceOperation: "update"
987
+ }, null);
988
+
989
+ await applyCrudRepositoryAfterWriteHook(
990
+ {
991
+ operation: "update",
992
+ recordId,
993
+ patch: {
994
+ ...dbPatch
995
+ },
996
+ record: updatedRecord,
997
+ output: updatedRecord
998
+ },
999
+ afterUpdateWrite,
1000
+ hookContextBase,
1001
+ {
1002
+ context: "crudRepositoryUpdateById",
1003
+ hookKey: "afterWrite"
1004
+ }
1005
+ );
1006
+
1007
+ return updatedRecord;
346
1008
  }
347
1009
 
348
- async function crudRepositoryDeleteById(runtime, knex, recordId, repositoryOptions = {}, callOptions = {}) {
1010
+ async function crudRepositoryDeleteById(runtime, knex, recordId, repositoryOptions = {}, callOptions = {}, hooks = null) {
349
1011
  const { client, tableName, idColumn, visible } = resolveCrudRepositoryCall(runtime, knex, repositoryOptions, callOptions);
1012
+ const methodHooks = normalizeCrudRepositoryHooks(hooks, ["modifyQuery", "finalizeOutput", "afterWrite"], {
1013
+ context: "crudRepositoryDeleteById"
1014
+ });
1015
+ const modifyDeleteByIdQuery = resolveOptionalCrudRepositoryHook(methodHooks, "modifyQuery", {
1016
+ context: "crudRepositoryDeleteById"
1017
+ });
1018
+ const finalizeDeleteOutput = resolveOptionalCrudRepositoryHook(methodHooks, "finalizeOutput", {
1019
+ context: "crudRepositoryDeleteById"
1020
+ });
1021
+ const afterDeleteWrite = resolveOptionalCrudRepositoryHook(methodHooks, "afterWrite", {
1022
+ context: "crudRepositoryDeleteById"
1023
+ });
1024
+ const hookContextBase = createCrudRepositoryHookContextBase(runtime, repositoryOptions, callOptions);
350
1025
  const existing = await crudRepositoryFindById(runtime, knex, recordId, repositoryOptions, {
351
1026
  ...callOptions,
352
1027
  include: "none",
353
1028
  trx: client
354
- });
1029
+ }, null);
355
1030
 
356
1031
  if (!existing) {
357
- return null;
1032
+ return applyCrudRepositoryOutputHook(
1033
+ null,
1034
+ finalizeDeleteOutput,
1035
+ {
1036
+ recordId,
1037
+ ...hookContextBase
1038
+ },
1039
+ {
1040
+ context: "crudRepositoryDeleteById",
1041
+ hookKey: "finalizeOutput",
1042
+ validateOutput(nextOutput) {
1043
+ if (nextOutput === null) {
1044
+ return;
1045
+ }
1046
+ if (!nextOutput || typeof nextOutput !== "object" || Array.isArray(nextOutput)) {
1047
+ throw new TypeError("crudRepositoryDeleteById hooks.finalizeOutput must return object or null.");
1048
+ }
1049
+ }
1050
+ }
1051
+ );
358
1052
  }
359
1053
 
360
- await client(tableName)
1054
+ let deleteQuery = client(tableName);
1055
+ const deleteHookResult = await applyCrudRepositoryQueryHook(
1056
+ deleteQuery,
1057
+ modifyDeleteByIdQuery,
1058
+ {
1059
+ recordId,
1060
+ existing,
1061
+ ...hookContextBase
1062
+ },
1063
+ {
1064
+ context: "crudRepositoryDeleteById",
1065
+ hookKey: "modifyQuery"
1066
+ }
1067
+ );
1068
+ deleteQuery = deleteHookResult.queryBuilder;
1069
+
1070
+ deleteQuery = deleteQuery
361
1071
  .where(visible)
362
1072
  .where({
363
1073
  [idColumn]: Number(recordId)
364
- })
365
- .delete();
1074
+ });
366
1075
 
367
- return {
1076
+ await deleteQuery.delete();
1077
+
1078
+ let output = {
368
1079
  id: existing.id,
369
1080
  deleted: true
370
1081
  };
1082
+
1083
+ output = await applyCrudRepositoryOutputHook(
1084
+ output,
1085
+ finalizeDeleteOutput,
1086
+ {
1087
+ recordId,
1088
+ ...hookContextBase
1089
+ },
1090
+ {
1091
+ context: "crudRepositoryDeleteById",
1092
+ hookKey: "finalizeOutput",
1093
+ validateOutput(nextOutput) {
1094
+ if (nextOutput === null) {
1095
+ return;
1096
+ }
1097
+ if (!nextOutput || typeof nextOutput !== "object" || Array.isArray(nextOutput)) {
1098
+ throw new TypeError("crudRepositoryDeleteById hooks.finalizeOutput must return object or null.");
1099
+ }
1100
+ }
1101
+ }
1102
+ );
1103
+
1104
+ await applyCrudRepositoryAfterWriteHook(
1105
+ {
1106
+ operation: "delete",
1107
+ recordId,
1108
+ existing,
1109
+ output
1110
+ },
1111
+ afterDeleteWrite,
1112
+ hookContextBase,
1113
+ {
1114
+ context: "crudRepositoryDeleteById",
1115
+ hookKey: "afterWrite"
1116
+ }
1117
+ );
1118
+
1119
+ return output;
371
1120
  }
372
1121
 
373
1122
  export {
@@ -375,6 +1124,7 @@ export {
375
1124
  crudRepositoryList,
376
1125
  crudRepositoryFindById,
377
1126
  crudRepositoryListByIds,
1127
+ crudRepositoryListByForeignIds,
378
1128
  crudRepositoryCreate,
379
1129
  crudRepositoryUpdateById,
380
1130
  crudRepositoryDeleteById