@wordrhyme/auto-crud-server 1.0.7 → 1.0.9

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { and, asc, desc, eq, gt, gte, ilike, inArray, isNull, lt, lte, ne, not, notIlike, notInArray, or, sql } from "drizzle-orm";
2
+ import { and, asc, desc, eq, getTableColumns, gt, gte, ilike, inArray, isNull, lt, lte, ne, not, notIlike, notInArray, or, sql } from "drizzle-orm";
3
3
  import { createInsertSchema, createSelectSchema } from "drizzle-zod";
4
4
  import { TRPCError, initTRPC } from "@trpc/server";
5
5
  import superjson from "superjson";
@@ -422,12 +422,25 @@ function resolveSoftDelete(option) {
422
422
  /**
423
423
  * 判断 procedure 配置的类型
424
424
  */
425
+ function isProcedureLike(config) {
426
+ if (!config || typeof config !== "object") return false;
427
+ const candidate = config;
428
+ return [
429
+ "input",
430
+ "output",
431
+ "query",
432
+ "mutation",
433
+ "use"
434
+ ].some((key) => typeof candidate[key] === "function");
435
+ }
425
436
  function isProcedureMap(config) {
426
437
  if (!config) return false;
427
438
  if (typeof config === "function") return false;
439
+ if (isProcedureLike(config)) return false;
428
440
  if (typeof config === "object" && config !== null) {
429
441
  const keys = Object.keys(config);
430
442
  const validKeys = [
443
+ "meta",
431
444
  "list",
432
445
  "get",
433
446
  "create",
@@ -473,6 +486,9 @@ function resolveConfig(config) {
473
486
  }
474
487
  };
475
488
  }
489
+ function resolveMetaProcedure(config, fallback) {
490
+ return isProcedureMap(config) ? config.meta ?? fallback : fallback;
491
+ }
476
492
  const filterItemSchema = z.object({
477
493
  id: z.string(),
478
494
  value: z.union([z.string(), z.array(z.string())]),
@@ -487,6 +503,7 @@ const baseListInputSchema = z.object({
487
503
  id: z.string(),
488
504
  desc: z.boolean()
489
505
  })).optional(),
506
+ search: z.string().trim().optional(),
490
507
  filters: z.array(filterItemSchema).optional(),
491
508
  joinOperator: z.enum(["and", "or"]).default("and")
492
509
  });
@@ -497,6 +514,7 @@ const baseExportInputSchema = z.object({
497
514
  id: z.string(),
498
515
  desc: z.boolean()
499
516
  })).optional(),
517
+ search: z.string().trim().optional(),
500
518
  filters: z.array(filterItemSchema).optional(),
501
519
  joinOperator: z.enum(["and", "or"]).default("and"),
502
520
  limit: z.number().min(1).optional()
@@ -514,6 +532,13 @@ const DEFAULT_OMIT_FIELDS = [
514
532
  "createdAt",
515
533
  "updatedAt"
516
534
  ];
535
+ function shouldEnableCrudExtensionSchema(config) {
536
+ return Boolean(config.id) && config.extensions !== false;
537
+ }
538
+ function withCrudExtensionInput(schema, config) {
539
+ if (!shouldEnableCrudExtensionSchema(config) || !isZodObject(schema)) return schema;
540
+ return schema.passthrough();
541
+ }
517
542
  /**
518
543
  * 解析并派生 Schema
519
544
  * - 优先使用显式传入的 Schema
@@ -531,11 +556,12 @@ function resolveSchemas(config) {
531
556
  const omitConfig = Object.fromEntries(omitFields.map((f) => [f, true]));
532
557
  schema = rawSchema.omit(omitConfig);
533
558
  }
559
+ schema = withCrudExtensionInput(schema, config);
534
560
  let selectSchema;
535
561
  if (config.selectSchema) selectSchema = config.selectSchema;
536
562
  else if (config.schema) selectSchema = schema;
537
563
  else selectSchema = createSelectSchema(table);
538
- const updateSchema = config.updateSchema ?? schema.partial().refine(nonEmpty, { message: "Update payload cannot be empty. At least one field is required." });
564
+ const updateSchema = config.updateSchema ? withCrudExtensionInput(config.updateSchema, config) : schema.partial().refine(nonEmpty, { message: "Update payload cannot be empty. At least one field is required." });
539
565
  return {
540
566
  selectSchema,
541
567
  schema,
@@ -580,6 +606,197 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
580
606
  }
581
607
  return getTableColumn(table, columnId);
582
608
  }
609
+ const CRUD_EXTENSION_FILTER_ID = "auto-crud-extension-filter";
610
+ const NO_CRUD_EXTENSION_MATCH = "__auto_crud_no_crud_extension_match__";
611
+ function isObjectRecord(value) {
612
+ return typeof value === "object" && value !== null && !Array.isArray(value);
613
+ }
614
+ function isCrudExtensionIdFilter(filter, idField) {
615
+ return filter.id === idField && filter.filterId === CRUD_EXTENSION_FILTER_ID;
616
+ }
617
+ function readCrudTarget(config) {
618
+ const id = config.id;
619
+ if (!id) return null;
620
+ return { id };
621
+ }
622
+ function resolveCrudExtensions(ctx, config) {
623
+ const extensions = config.extensions;
624
+ if (extensions === false) return null;
625
+ if (typeof extensions === "function") return extensions(ctx) ?? null;
626
+ if (extensions) return extensions;
627
+ if (!config.id) return null;
628
+ return ctx?.crudExtensions ?? null;
629
+ }
630
+ function getTableColumnNames(table) {
631
+ try {
632
+ const columns = getTableColumns(table);
633
+ const names = Object.keys(columns);
634
+ if (names.length > 0) return new Set(names);
635
+ } catch {}
636
+ return new Set(Object.keys(table).filter((key) => !key.startsWith("_")));
637
+ }
638
+ function splitCrudExtensionWriteInput(inputData, tableColumnNames) {
639
+ if (!isObjectRecord(inputData)) return {
640
+ data: inputData,
641
+ rawValues: {},
642
+ baseValues: {},
643
+ extraValues: null
644
+ };
645
+ const baseValues = {};
646
+ const extraValues = {};
647
+ for (const [key, value] of Object.entries(inputData)) {
648
+ if (key === "ext") {
649
+ if (isObjectRecord(value)) Object.assign(extraValues, value);
650
+ continue;
651
+ }
652
+ if (tableColumnNames.has(key)) baseValues[key] = value;
653
+ else extraValues[key] = value;
654
+ }
655
+ return {
656
+ data: baseValues,
657
+ rawValues: inputData,
658
+ baseValues,
659
+ extraValues: Object.keys(extraValues).length > 0 ? extraValues : null
660
+ };
661
+ }
662
+ async function saveCrudExtraValues(ctx, config, entityId, input) {
663
+ const target = readCrudTarget(config);
664
+ const extraValues = input.extraValues;
665
+ if (!target || !extraValues) return;
666
+ if (entityId === null || entityId === void 0 || String(entityId).length === 0) return;
667
+ const provider = resolveCrudExtensions(ctx, config);
668
+ if (!provider?.saveExtraValues) throw new TRPCError({
669
+ code: "PRECONDITION_FAILED",
670
+ message: "CRUD extension persistence is not available"
671
+ });
672
+ await provider.saveExtraValues({
673
+ id: target.id,
674
+ entityId: String(entityId),
675
+ rawValues: input.rawValues,
676
+ baseValues: input.baseValues,
677
+ extraValues,
678
+ tx: ctx?.tx
679
+ });
680
+ }
681
+ function assertCrudExtraValuesWritable(ctx, config, input) {
682
+ if (!readCrudTarget(config) || !input.extraValues) return;
683
+ if (!resolveCrudExtensions(ctx, config)?.saveExtraValues) throw new TRPCError({
684
+ code: "PRECONDITION_FAILED",
685
+ message: "CRUD extension persistence is not available"
686
+ });
687
+ }
688
+ function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
689
+ if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
690
+ }
691
+ async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
692
+ const extraByInputId = /* @__PURE__ */ new Map();
693
+ for (const input of inputs) {
694
+ if (!input.extraValues || !isObjectRecord(input.data)) continue;
695
+ const inputId = input.data[idField];
696
+ if (inputId !== null && inputId !== void 0) extraByInputId.set(String(inputId), input);
697
+ }
698
+ const canUseIndexFallback = results.length === inputs.length;
699
+ await Promise.all(results.map((result, index) => {
700
+ const resultId = result[idField];
701
+ const fallbackInput = canUseIndexFallback ? inputs[index] : void 0;
702
+ const fallbackInputId = fallbackInput && isObjectRecord(fallbackInput.data) ? fallbackInput.data[idField] : void 0;
703
+ const entityId = resultId !== null && resultId !== void 0 ? resultId : fallbackInputId;
704
+ const input = entityId !== null && entityId !== void 0 ? extraByInputId.get(String(entityId)) ?? fallbackInput : fallbackInput;
705
+ return input ? saveCrudExtraValues(ctx, config, entityId, input) : Promise.resolve();
706
+ }));
707
+ }
708
+ function readProjectionDisplay(value) {
709
+ if (!isObjectRecord(value)) return value;
710
+ if ("display" in value && value.display !== null && value.display !== void 0) return value.display;
711
+ if ("value" in value) return value.value;
712
+ return value;
713
+ }
714
+ async function enrichCrudRows(ctx, config, idField, rows) {
715
+ const target = readCrudTarget(config);
716
+ if (!target || rows.length === 0) return rows;
717
+ const provider = resolveCrudExtensions(ctx, config);
718
+ if (!provider?.readProjection) return rows;
719
+ const entityIds = rows.map((row) => row[idField]).filter((id) => typeof id === "string" && id.length > 0 || typeof id === "number").map(String);
720
+ if (entityIds.length === 0) return rows;
721
+ const projections = await provider.readProjection({
722
+ id: target.id,
723
+ entityIds
724
+ });
725
+ return rows.map((row) => {
726
+ const rowId = row[idField];
727
+ const projected = rowId !== null && rowId !== void 0 ? projections[String(rowId)] : void 0;
728
+ if (!projected || Object.keys(projected).length === 0) return row;
729
+ return {
730
+ ...row,
731
+ ...Object.fromEntries(Object.entries(projected).map(([field, value]) => [field, readProjectionDisplay(value)]))
732
+ };
733
+ });
734
+ }
735
+ function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
736
+ if (allowedColumns && allowedColumns.length > 0) return resolveColumnTarget({
737
+ table,
738
+ columnId,
739
+ allowedColumns
740
+ }) !== void 0;
741
+ return getTableColumn(table, columnId) !== void 0;
742
+ }
743
+ function isKnownBaseCrudColumn(table, columnId, allowedColumns) {
744
+ return getTableColumn(table, columnId) !== void 0 || findColumnRef(columnId, allowedColumns) !== void 0;
745
+ }
746
+ async function applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, input) {
747
+ const target = readCrudTarget(config);
748
+ const filters = input.filters ?? [];
749
+ const search = typeof input.search === "string" ? input.search.trim() : "";
750
+ if (!target || filters.length === 0 && !search) return input;
751
+ const baseFilters = [];
752
+ const extensionFilters = [];
753
+ for (const filter of filters) if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
754
+ else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
755
+ else extensionFilters.push(filter);
756
+ if (extensionFilters.length === 0 && !search) return {
757
+ ...input,
758
+ filters: baseFilters
759
+ };
760
+ const provider = resolveCrudExtensions(ctx, config);
761
+ const matchedSets = [];
762
+ if (extensionFilters.length > 0) {
763
+ if (!provider?.matchEntityIds) throw new TRPCError({
764
+ code: "PRECONDITION_FAILED",
765
+ message: "CRUD extension filtering is not available"
766
+ });
767
+ const matchedIds$1 = await provider.matchEntityIds({
768
+ id: target.id,
769
+ filters: extensionFilters,
770
+ joinOperator: input.joinOperator,
771
+ limit: 5e3
772
+ });
773
+ matchedSets.push(new Set(matchedIds$1));
774
+ }
775
+ if (search && provider?.searchEntityIds) {
776
+ const matchedIds$1 = await provider.searchEntityIds({
777
+ id: target.id,
778
+ search,
779
+ limit: 5e3
780
+ });
781
+ matchedSets.push(new Set(matchedIds$1));
782
+ }
783
+ if (matchedSets.length === 0) return {
784
+ ...input,
785
+ filters: baseFilters
786
+ };
787
+ let matchedIds = matchedSets[0] ? [...matchedSets[0]] : [];
788
+ for (const set of matchedSets.slice(1)) matchedIds = matchedIds.filter((id) => set.has(id));
789
+ return {
790
+ ...input,
791
+ filters: [...baseFilters, {
792
+ id: idField,
793
+ value: matchedIds.length > 0 ? matchedIds : [NO_CRUD_EXTENSION_MATCH],
794
+ variant: "multiSelect",
795
+ operator: "inArray",
796
+ filterId: CRUD_EXTENSION_FILTER_ID
797
+ }]
798
+ };
799
+ }
583
800
  /**
584
801
  * 创建 CRUD Router
585
802
  *
@@ -595,14 +812,19 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
595
812
  function createCrudRouter(config) {
596
813
  const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
597
814
  const { selectSchema, schema, updateSchema } = resolveSchemas(config);
598
- const entityOutputSchema = selectSchema;
815
+ const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
599
816
  const listOutputSchema = z.object({
600
- data: z.array(selectSchema),
817
+ data: z.array(entityOutputSchema),
601
818
  total: z.number(),
602
819
  page: z.number(),
603
820
  perPage: z.number(),
604
821
  pageCount: z.number()
605
822
  });
823
+ const metadataOutputSchema = z.object({
824
+ schema: z.unknown().optional(),
825
+ fields: z.record(z.string(), z.unknown()).optional(),
826
+ errors: z.array(z.string()).optional()
827
+ });
606
828
  const deleteManyOutputSchema = z.object({ deleted: z.number() });
607
829
  const updateManyOutputSchema = z.object({ updated: z.number() });
608
830
  const upsertOutputSchema = z.object({
@@ -610,12 +832,12 @@ function createCrudRouter(config) {
610
832
  isNew: z.boolean()
611
833
  });
612
834
  const exportOutputSchema = z.object({
613
- data: z.array(selectSchema),
835
+ data: z.array(entityOutputSchema),
614
836
  total: z.number(),
615
837
  hasMore: z.boolean()
616
838
  });
617
839
  const createManyOutputSchema = z.object({
618
- created: z.array(selectSchema),
840
+ created: z.array(entityOutputSchema),
619
841
  count: z.number()
620
842
  });
621
843
  const importOutputSchema = z.object({
@@ -628,12 +850,14 @@ function createCrudRouter(config) {
628
850
  }))
629
851
  });
630
852
  const resolved = resolveConfig(config);
853
+ const metaProcedure = resolveMetaProcedure(config.procedure, resolved.procedureFactory("list"));
631
854
  const softDelete = resolveSoftDelete(softDeleteOption);
632
855
  const resolvedListInputSchema = config.listInputSchema ?? baseListInputSchema;
633
856
  const resolvedGetInputSchema = config.getInputSchema ? z.union([z.string(), config.getInputSchema]) : defaultGetInputSchema;
634
857
  const resolvedExportInputSchema = config.exportInputSchema ?? baseExportInputSchema;
635
858
  const m = middleware;
636
859
  const getIdColumn = () => table[idField];
860
+ const tableColumnNames = getTableColumnNames(table);
637
861
  const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
638
862
  const buildWhere = (ctx, operation, additionalCondition) => {
639
863
  const conditions = [];
@@ -673,25 +897,38 @@ function createCrudRouter(config) {
673
897
  });
674
898
  };
675
899
  const procedures = {
900
+ meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
901
+ await withGuard(ctx, "list");
902
+ const target = readCrudTarget(config);
903
+ if (!target) return {};
904
+ const provider = resolveCrudExtensions(ctx, config);
905
+ if (!provider?.getMetadata) return {};
906
+ return provider.getMetadata({ id: target.id });
907
+ }),
676
908
  list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
677
909
  await withGuard(ctx, "list");
678
910
  const doList = async (listInput$1) => {
679
- const offset = (listInput$1.page - 1) * listInput$1.perPage;
680
- const validatedFilters = listInput$1.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
911
+ const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, listInput$1);
912
+ const offset = (effectiveInput.page - 1) * effectiveInput.perPage;
913
+ const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
681
914
  const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
682
915
  table,
683
916
  filters: validatedFilters,
684
- joinOperator: listInput$1.joinOperator,
685
- resolveColumn: (columnId) => resolveColumnTarget({
686
- table,
687
- columnId: String(columnId),
688
- allowedColumns: filterableColumns
689
- })
917
+ joinOperator: effectiveInput.joinOperator,
918
+ resolveColumn: (columnId) => {
919
+ const id = String(columnId);
920
+ if (id === idField) return getTableColumn(table, idField);
921
+ return resolveColumnTarget({
922
+ table,
923
+ columnId: id,
924
+ allowedColumns: filterableColumns
925
+ });
926
+ }
690
927
  }) : void 0);
691
928
  let query = ctx.db.select().from(table).$dynamic();
692
929
  if (where) query = query.where(where);
693
- if (listInput$1.sort?.length) {
694
- const sortField = listInput$1.sort[0];
930
+ if (effectiveInput.sort?.length) {
931
+ const sortField = effectiveInput.sort[0];
695
932
  if (sortField && validateColumn(sortField.id, sortableColumns)) {
696
933
  const column = resolveColumnTarget({
697
934
  table,
@@ -701,16 +938,16 @@ function createCrudRouter(config) {
701
938
  if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
702
939
  }
703
940
  }
704
- const data = await query.limit(listInput$1.perPage).offset(offset);
941
+ const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveInput.perPage).offset(offset));
705
942
  let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
706
943
  if (where) countQuery = countQuery.where(where);
707
944
  const count = (await countQuery)[0]?.count ?? 0;
708
945
  return {
709
- data,
946
+ data: enrichedData,
710
947
  total: count,
711
- page: listInput$1.page,
712
- perPage: listInput$1.perPage,
713
- pageCount: Math.ceil(count / listInput$1.perPage)
948
+ page: effectiveInput.page,
949
+ perPage: effectiveInput.perPage,
950
+ pageCount: Math.ceil(count / effectiveInput.perPage)
714
951
  };
715
952
  };
716
953
  const listInput = input;
@@ -728,7 +965,9 @@ function createCrudRouter(config) {
728
965
  const where = buildWhere(ctx, "get", eq(getIdColumn(), id$1));
729
966
  const [item] = await ctx.db.select().from(table).where(where);
730
967
  await withAuthorize(ctx, item, "get");
731
- return item ?? null;
968
+ if (!item) return null;
969
+ const [enriched] = await enrichCrudRows(ctx, config, idField, [item]);
970
+ return enriched ?? null;
732
971
  };
733
972
  const id = typeof input === "string" ? input : input.id;
734
973
  if (m.get) return m.get({
@@ -742,12 +981,15 @@ function createCrudRouter(config) {
742
981
  create: resolved.procedureFactory("create").input(schema).output(entityOutputSchema).mutation(async ({ ctx, input }) => {
743
982
  await withGuard(ctx, "create");
744
983
  const doCreate = async (inputData) => {
984
+ const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
985
+ assertCrudExtraValuesWritable(ctx, config, splitInput);
745
986
  const injectData = resolved.getInject(ctx, "create");
746
987
  const data = {
747
- ...inputData,
988
+ ...splitInput.data,
748
989
  ...injectData
749
990
  };
750
991
  const [created] = await ctx.db.insert(table).values(data).returning();
992
+ await saveCrudExtraValues(ctx, config, created?.[idField], splitInput);
751
993
  return created;
752
994
  };
753
995
  if (m.create) return m.create({
@@ -770,12 +1012,16 @@ function createCrudRouter(config) {
770
1012
  message: "Resource not found or access denied"
771
1013
  });
772
1014
  const doUpdate = async (updateData) => {
1015
+ const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
1016
+ assertCrudExtraValuesWritable(ctx, config, splitUpdate);
773
1017
  const injectData = resolved.getInject(ctx, "update");
774
1018
  const data = {
775
- ...updateData,
1019
+ ...splitUpdate.data,
776
1020
  ...injectData
777
1021
  };
778
- const [updated] = await ctx.db.update(table).set(data).where(where).returning();
1022
+ let updated = existing;
1023
+ if (Object.keys(data).length > 0) [updated] = await ctx.db.update(table).set(data).where(where).returning();
1024
+ await saveCrudExtraValues(ctx, config, input.id, splitUpdate);
779
1025
  return updated;
780
1026
  };
781
1027
  if (m.update) return m.update({
@@ -831,12 +1077,18 @@ function createCrudRouter(config) {
831
1077
  await withGuard(ctx, "updateMany");
832
1078
  const where = buildWhere(ctx, "updateMany", inArray(getIdColumn(), input.ids));
833
1079
  const doUpdateMany = async (updateData) => {
1080
+ const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
1081
+ assertCrudExtraValuesWritable(ctx, config, splitUpdate);
834
1082
  const injectData = resolved.getInject(ctx, "update");
835
1083
  const data = {
836
- ...updateData,
1084
+ ...splitUpdate.data,
837
1085
  ...injectData
838
1086
  };
839
- return { updated: (await ctx.db.update(table).set(data).where(where).returning({ id: getIdColumn() })).length };
1087
+ let updated = input.ids.map((id) => ({ [idField]: id }));
1088
+ if (Object.keys(data).length > 0) updated = await ctx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
1089
+ else updated = await ctx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
1090
+ await Promise.all(updated.map((row) => saveCrudExtraValues(ctx, config, row[idField], splitUpdate)));
1091
+ return { updated: updated.length };
840
1092
  };
841
1093
  if (m.updateMany) return m.updateMany({
842
1094
  ctx,
@@ -849,7 +1101,9 @@ function createCrudRouter(config) {
849
1101
  upsert: resolved.procedureFactory("upsert").input(schema).output(upsertOutputSchema).mutation(async ({ ctx, input }) => {
850
1102
  await withGuard(ctx, "upsert");
851
1103
  const doUpsert = async (inputData) => {
852
- const inputId = inputData[idField];
1104
+ const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
1105
+ assertCrudExtraValuesWritable(ctx, config, splitInput);
1106
+ const inputId = splitInput.data[idField];
853
1107
  let isNew = true;
854
1108
  let existing = null;
855
1109
  if (inputId) {
@@ -861,16 +1115,17 @@ function createCrudRouter(config) {
861
1115
  }
862
1116
  const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
863
1117
  const data = {
864
- ...inputData,
1118
+ ...splitInput.data,
865
1119
  ...injectData
866
1120
  };
867
1121
  const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
868
1122
  target: getIdColumn(),
869
1123
  set: {
870
- ...inputData,
1124
+ ...splitInput.data,
871
1125
  ...resolved.getInject(ctx, "update")
872
1126
  }
873
1127
  }).returning();
1128
+ await saveCrudExtraValues(ctx, config, result?.[idField] ?? inputId, splitInput);
874
1129
  return {
875
1130
  data: result,
876
1131
  isNew
@@ -886,22 +1141,32 @@ function createCrudRouter(config) {
886
1141
  export: resolved.procedureFactory("export").input(resolvedExportInputSchema).output(exportOutputSchema).query(async ({ ctx, input }) => {
887
1142
  await withGuard(ctx, "export");
888
1143
  const doExport = async (exportInput$1) => {
889
- const effectiveLimit = Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize);
890
- const validatedFilters = exportInput$1.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
1144
+ const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, {
1145
+ page: 1,
1146
+ perPage: Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize),
1147
+ joinOperator: exportInput$1.joinOperator ?? "and",
1148
+ ...exportInput$1
1149
+ });
1150
+ const effectiveLimit = Math.min(Math.max(effectiveInput.limit ?? maxExportSize, 1), maxExportSize);
1151
+ const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
891
1152
  const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
892
1153
  table,
893
1154
  filters: validatedFilters,
894
- joinOperator: exportInput$1.joinOperator ?? "and",
895
- resolveColumn: (columnId) => resolveColumnTarget({
896
- table,
897
- columnId: String(columnId),
898
- allowedColumns: filterableColumns
899
- })
1155
+ joinOperator: effectiveInput.joinOperator ?? "and",
1156
+ resolveColumn: (columnId) => {
1157
+ const id = String(columnId);
1158
+ if (id === idField) return getTableColumn(table, idField);
1159
+ return resolveColumnTarget({
1160
+ table,
1161
+ columnId: id,
1162
+ allowedColumns: filterableColumns
1163
+ });
1164
+ }
900
1165
  }) : void 0);
901
1166
  let query = ctx.db.select().from(table).$dynamic();
902
1167
  if (where) query = query.where(where);
903
- if (exportInput$1.sort?.length) {
904
- const sortField = exportInput$1.sort[0];
1168
+ if (effectiveInput.sort?.length) {
1169
+ const sortField = effectiveInput.sort[0];
905
1170
  if (sortField && validateColumn(sortField.id, sortableColumns)) {
906
1171
  const column = resolveColumnTarget({
907
1172
  table,
@@ -911,14 +1176,14 @@ function createCrudRouter(config) {
911
1176
  if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
912
1177
  }
913
1178
  }
914
- const data = await query.limit(effectiveLimit);
1179
+ const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveLimit));
915
1180
  let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
916
1181
  if (where) countQuery = countQuery.where(where);
917
1182
  const total = (await countQuery)[0]?.count ?? 0;
918
1183
  return {
919
- data,
1184
+ data: enrichedData,
920
1185
  total,
921
- hasMore: total > data.length
1186
+ hasMore: total > enrichedData.length
922
1187
  };
923
1188
  };
924
1189
  const exportInput = input;
@@ -932,12 +1197,15 @@ function createCrudRouter(config) {
932
1197
  createMany: resolved.procedureFactory("createMany").input(z.array(schema).min(1).max(maxBatchSize)).output(createManyOutputSchema).mutation(async ({ ctx, input }) => {
933
1198
  await withGuard(ctx, "createMany");
934
1199
  const doCreateMany = async (items) => {
1200
+ const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
1201
+ assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
935
1202
  const injectData = resolved.getInject(ctx, "create");
936
- const values = items.map((item) => ({
937
- ...item,
1203
+ const values = splitItems.map(({ data }) => ({
1204
+ ...data,
938
1205
  ...injectData
939
1206
  }));
940
1207
  const created = await ctx.db.insert(table).values(values).onConflictDoNothing().returning();
1208
+ await saveCrudExtraValuesForRows(ctx, config, idField, created, splitItems);
941
1209
  return {
942
1210
  created,
943
1211
  count: created.length
@@ -958,8 +1226,10 @@ function createCrudRouter(config) {
958
1226
  for (let i = 0; i < importData$1.rows.length; i++) {
959
1227
  const row = importData$1.rows[i];
960
1228
  const result = schema.safeParse(row);
961
- if (result.success) validRows.push(result.data);
962
- else {
1229
+ if (result.success) {
1230
+ const splitRow = splitCrudExtensionWriteInput(result.data, tableColumnNames);
1231
+ validRows.push(splitRow);
1232
+ } else {
963
1233
  const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
964
1234
  failed.push({
965
1235
  row: i,
@@ -979,26 +1249,30 @@ function createCrudRouter(config) {
979
1249
  skipped: 0,
980
1250
  failed
981
1251
  };
1252
+ assertCrudExtraValuesWritableForRows(ctx, config, validRows);
982
1253
  const injectData = resolved.getInject(ctx, "create");
983
1254
  const values = validRows.map((row) => ({
984
- ...row,
1255
+ ...row.data,
985
1256
  ...injectData
986
1257
  }));
987
1258
  let insertedCount = 0;
988
1259
  let updatedCount = 0;
989
1260
  if (importData$1.onConflict === "upsert") {
990
1261
  const existingCount = (await ctx.db.select({ id: getIdColumn() }).from(table).where(inArray(getIdColumn(), values.map((v) => v[idField]).filter(Boolean)))).length;
991
- const updateFields = Object.keys(validRows[0]);
1262
+ const updateFields = Object.keys(validRows[0].data);
992
1263
  const setClause = { ...resolved.getInject(ctx, "update") };
993
1264
  for (const key of updateFields) setClause[key] = sql.raw(`excluded."${key}"`);
994
1265
  const results = await ctx.db.insert(table).values(values).onConflictDoUpdate({
995
1266
  target: getIdColumn(),
996
1267
  set: setClause
997
1268
  }).returning({ id: getIdColumn() });
1269
+ await saveCrudExtraValuesForRows(ctx, config, idField, results.map((result) => ({ [idField]: result.id })), validRows);
998
1270
  updatedCount = Math.min(existingCount, results.length);
999
1271
  insertedCount = results.length - updatedCount;
1000
1272
  } else {
1001
- insertedCount = (await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() })).length;
1273
+ const inserted = await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() });
1274
+ await saveCrudExtraValuesForRows(ctx, config, idField, inserted.map((result) => ({ [idField]: result.id })), validRows);
1275
+ insertedCount = inserted.length;
1002
1276
  updatedCount = 0;
1003
1277
  }
1004
1278
  const skipped = validRows.length - insertedCount - updatedCount;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wordrhyme/auto-crud-server",
3
3
  "type": "module",
4
- "version": "1.0.7",
4
+ "version": "1.0.9",
5
5
  "description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
6
6
  "author": "wordrhyme",
7
7
  "license": "MIT",
@@ -38,20 +38,20 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@trpc/server": "^11.0.0",
41
- "@types/node": "^22.19.3",
41
+ "@types/node": "^22.19.17",
42
42
  "drizzle-orm": "1.0.0-beta.15-859cf75",
43
43
  "drizzle-zod": "1.0.0-beta.14-a36c63d",
44
- "eslint": "^9.39.2",
44
+ "eslint": "^9.39.4",
45
45
  "superjson": "^2.2.6",
46
46
  "tsdown": "^0.15.12",
47
47
  "typescript": "^5.9.3",
48
48
  "vitest": "^4.0.18",
49
- "zod": "^4.1.13",
49
+ "zod": "^4.3.6",
50
50
  "@internal/eslint-config": "0.3.0",
51
- "@internal/tsconfig": "0.1.0",
52
51
  "@internal/prettier-config": "0.0.1",
53
52
  "@internal/tsdown-config": "0.1.0",
54
- "@internal/vitest-config": "0.1.0"
53
+ "@internal/vitest-config": "0.1.0",
54
+ "@internal/tsconfig": "0.1.0"
55
55
  },
56
56
  "prettier": "@internal/prettier-config",
57
57
  "scripts": {