@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.cjs CHANGED
@@ -451,12 +451,25 @@ function resolveSoftDelete(option) {
451
451
  /**
452
452
  * 判断 procedure 配置的类型
453
453
  */
454
+ function isProcedureLike(config) {
455
+ if (!config || typeof config !== "object") return false;
456
+ const candidate = config;
457
+ return [
458
+ "input",
459
+ "output",
460
+ "query",
461
+ "mutation",
462
+ "use"
463
+ ].some((key) => typeof candidate[key] === "function");
464
+ }
454
465
  function isProcedureMap(config) {
455
466
  if (!config) return false;
456
467
  if (typeof config === "function") return false;
468
+ if (isProcedureLike(config)) return false;
457
469
  if (typeof config === "object" && config !== null) {
458
470
  const keys = Object.keys(config);
459
471
  const validKeys = [
472
+ "meta",
460
473
  "list",
461
474
  "get",
462
475
  "create",
@@ -502,6 +515,9 @@ function resolveConfig(config) {
502
515
  }
503
516
  };
504
517
  }
518
+ function resolveMetaProcedure(config, fallback) {
519
+ return isProcedureMap(config) ? config.meta ?? fallback : fallback;
520
+ }
505
521
  const filterItemSchema = zod.z.object({
506
522
  id: zod.z.string(),
507
523
  value: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]),
@@ -516,6 +532,7 @@ const baseListInputSchema = zod.z.object({
516
532
  id: zod.z.string(),
517
533
  desc: zod.z.boolean()
518
534
  })).optional(),
535
+ search: zod.z.string().trim().optional(),
519
536
  filters: zod.z.array(filterItemSchema).optional(),
520
537
  joinOperator: zod.z.enum(["and", "or"]).default("and")
521
538
  });
@@ -526,6 +543,7 @@ const baseExportInputSchema = zod.z.object({
526
543
  id: zod.z.string(),
527
544
  desc: zod.z.boolean()
528
545
  })).optional(),
546
+ search: zod.z.string().trim().optional(),
529
547
  filters: zod.z.array(filterItemSchema).optional(),
530
548
  joinOperator: zod.z.enum(["and", "or"]).default("and"),
531
549
  limit: zod.z.number().min(1).optional()
@@ -543,6 +561,13 @@ const DEFAULT_OMIT_FIELDS = [
543
561
  "createdAt",
544
562
  "updatedAt"
545
563
  ];
564
+ function shouldEnableCrudExtensionSchema(config) {
565
+ return Boolean(config.id) && config.extensions !== false;
566
+ }
567
+ function withCrudExtensionInput(schema, config) {
568
+ if (!shouldEnableCrudExtensionSchema(config) || !isZodObject(schema)) return schema;
569
+ return schema.passthrough();
570
+ }
546
571
  /**
547
572
  * 解析并派生 Schema
548
573
  * - 优先使用显式传入的 Schema
@@ -560,11 +585,12 @@ function resolveSchemas(config) {
560
585
  const omitConfig = Object.fromEntries(omitFields.map((f) => [f, true]));
561
586
  schema = rawSchema.omit(omitConfig);
562
587
  }
588
+ schema = withCrudExtensionInput(schema, config);
563
589
  let selectSchema;
564
590
  if (config.selectSchema) selectSchema = config.selectSchema;
565
591
  else if (config.schema) selectSchema = schema;
566
592
  else selectSchema = (0, drizzle_zod.createSelectSchema)(table);
567
- const updateSchema = config.updateSchema ?? schema.partial().refine(nonEmpty, { message: "Update payload cannot be empty. At least one field is required." });
593
+ const updateSchema = config.updateSchema ? withCrudExtensionInput(config.updateSchema, config) : schema.partial().refine(nonEmpty, { message: "Update payload cannot be empty. At least one field is required." });
568
594
  return {
569
595
  selectSchema,
570
596
  schema,
@@ -609,6 +635,197 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
609
635
  }
610
636
  return getTableColumn(table, columnId);
611
637
  }
638
+ const CRUD_EXTENSION_FILTER_ID = "auto-crud-extension-filter";
639
+ const NO_CRUD_EXTENSION_MATCH = "__auto_crud_no_crud_extension_match__";
640
+ function isObjectRecord(value) {
641
+ return typeof value === "object" && value !== null && !Array.isArray(value);
642
+ }
643
+ function isCrudExtensionIdFilter(filter, idField) {
644
+ return filter.id === idField && filter.filterId === CRUD_EXTENSION_FILTER_ID;
645
+ }
646
+ function readCrudTarget(config) {
647
+ const id = config.id;
648
+ if (!id) return null;
649
+ return { id };
650
+ }
651
+ function resolveCrudExtensions(ctx, config) {
652
+ const extensions = config.extensions;
653
+ if (extensions === false) return null;
654
+ if (typeof extensions === "function") return extensions(ctx) ?? null;
655
+ if (extensions) return extensions;
656
+ if (!config.id) return null;
657
+ return ctx?.crudExtensions ?? null;
658
+ }
659
+ function getTableColumnNames(table) {
660
+ try {
661
+ const columns = (0, drizzle_orm.getTableColumns)(table);
662
+ const names = Object.keys(columns);
663
+ if (names.length > 0) return new Set(names);
664
+ } catch {}
665
+ return new Set(Object.keys(table).filter((key) => !key.startsWith("_")));
666
+ }
667
+ function splitCrudExtensionWriteInput(inputData, tableColumnNames) {
668
+ if (!isObjectRecord(inputData)) return {
669
+ data: inputData,
670
+ rawValues: {},
671
+ baseValues: {},
672
+ extraValues: null
673
+ };
674
+ const baseValues = {};
675
+ const extraValues = {};
676
+ for (const [key, value] of Object.entries(inputData)) {
677
+ if (key === "ext") {
678
+ if (isObjectRecord(value)) Object.assign(extraValues, value);
679
+ continue;
680
+ }
681
+ if (tableColumnNames.has(key)) baseValues[key] = value;
682
+ else extraValues[key] = value;
683
+ }
684
+ return {
685
+ data: baseValues,
686
+ rawValues: inputData,
687
+ baseValues,
688
+ extraValues: Object.keys(extraValues).length > 0 ? extraValues : null
689
+ };
690
+ }
691
+ async function saveCrudExtraValues(ctx, config, entityId, input) {
692
+ const target = readCrudTarget(config);
693
+ const extraValues = input.extraValues;
694
+ if (!target || !extraValues) return;
695
+ if (entityId === null || entityId === void 0 || String(entityId).length === 0) return;
696
+ const provider = resolveCrudExtensions(ctx, config);
697
+ if (!provider?.saveExtraValues) throw new __trpc_server.TRPCError({
698
+ code: "PRECONDITION_FAILED",
699
+ message: "CRUD extension persistence is not available"
700
+ });
701
+ await provider.saveExtraValues({
702
+ id: target.id,
703
+ entityId: String(entityId),
704
+ rawValues: input.rawValues,
705
+ baseValues: input.baseValues,
706
+ extraValues,
707
+ tx: ctx?.tx
708
+ });
709
+ }
710
+ function assertCrudExtraValuesWritable(ctx, config, input) {
711
+ if (!readCrudTarget(config) || !input.extraValues) return;
712
+ if (!resolveCrudExtensions(ctx, config)?.saveExtraValues) throw new __trpc_server.TRPCError({
713
+ code: "PRECONDITION_FAILED",
714
+ message: "CRUD extension persistence is not available"
715
+ });
716
+ }
717
+ function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
718
+ if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
719
+ }
720
+ async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
721
+ const extraByInputId = /* @__PURE__ */ new Map();
722
+ for (const input of inputs) {
723
+ if (!input.extraValues || !isObjectRecord(input.data)) continue;
724
+ const inputId = input.data[idField];
725
+ if (inputId !== null && inputId !== void 0) extraByInputId.set(String(inputId), input);
726
+ }
727
+ const canUseIndexFallback = results.length === inputs.length;
728
+ await Promise.all(results.map((result, index) => {
729
+ const resultId = result[idField];
730
+ const fallbackInput = canUseIndexFallback ? inputs[index] : void 0;
731
+ const fallbackInputId = fallbackInput && isObjectRecord(fallbackInput.data) ? fallbackInput.data[idField] : void 0;
732
+ const entityId = resultId !== null && resultId !== void 0 ? resultId : fallbackInputId;
733
+ const input = entityId !== null && entityId !== void 0 ? extraByInputId.get(String(entityId)) ?? fallbackInput : fallbackInput;
734
+ return input ? saveCrudExtraValues(ctx, config, entityId, input) : Promise.resolve();
735
+ }));
736
+ }
737
+ function readProjectionDisplay(value) {
738
+ if (!isObjectRecord(value)) return value;
739
+ if ("display" in value && value.display !== null && value.display !== void 0) return value.display;
740
+ if ("value" in value) return value.value;
741
+ return value;
742
+ }
743
+ async function enrichCrudRows(ctx, config, idField, rows) {
744
+ const target = readCrudTarget(config);
745
+ if (!target || rows.length === 0) return rows;
746
+ const provider = resolveCrudExtensions(ctx, config);
747
+ if (!provider?.readProjection) return rows;
748
+ const entityIds = rows.map((row) => row[idField]).filter((id) => typeof id === "string" && id.length > 0 || typeof id === "number").map(String);
749
+ if (entityIds.length === 0) return rows;
750
+ const projections = await provider.readProjection({
751
+ id: target.id,
752
+ entityIds
753
+ });
754
+ return rows.map((row) => {
755
+ const rowId = row[idField];
756
+ const projected = rowId !== null && rowId !== void 0 ? projections[String(rowId)] : void 0;
757
+ if (!projected || Object.keys(projected).length === 0) return row;
758
+ return {
759
+ ...row,
760
+ ...Object.fromEntries(Object.entries(projected).map(([field, value]) => [field, readProjectionDisplay(value)]))
761
+ };
762
+ });
763
+ }
764
+ function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
765
+ if (allowedColumns && allowedColumns.length > 0) return resolveColumnTarget({
766
+ table,
767
+ columnId,
768
+ allowedColumns
769
+ }) !== void 0;
770
+ return getTableColumn(table, columnId) !== void 0;
771
+ }
772
+ function isKnownBaseCrudColumn(table, columnId, allowedColumns) {
773
+ return getTableColumn(table, columnId) !== void 0 || findColumnRef(columnId, allowedColumns) !== void 0;
774
+ }
775
+ async function applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, input) {
776
+ const target = readCrudTarget(config);
777
+ const filters = input.filters ?? [];
778
+ const search = typeof input.search === "string" ? input.search.trim() : "";
779
+ if (!target || filters.length === 0 && !search) return input;
780
+ const baseFilters = [];
781
+ const extensionFilters = [];
782
+ for (const filter of filters) if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
783
+ else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
784
+ else extensionFilters.push(filter);
785
+ if (extensionFilters.length === 0 && !search) return {
786
+ ...input,
787
+ filters: baseFilters
788
+ };
789
+ const provider = resolveCrudExtensions(ctx, config);
790
+ const matchedSets = [];
791
+ if (extensionFilters.length > 0) {
792
+ if (!provider?.matchEntityIds) throw new __trpc_server.TRPCError({
793
+ code: "PRECONDITION_FAILED",
794
+ message: "CRUD extension filtering is not available"
795
+ });
796
+ const matchedIds$1 = await provider.matchEntityIds({
797
+ id: target.id,
798
+ filters: extensionFilters,
799
+ joinOperator: input.joinOperator,
800
+ limit: 5e3
801
+ });
802
+ matchedSets.push(new Set(matchedIds$1));
803
+ }
804
+ if (search && provider?.searchEntityIds) {
805
+ const matchedIds$1 = await provider.searchEntityIds({
806
+ id: target.id,
807
+ search,
808
+ limit: 5e3
809
+ });
810
+ matchedSets.push(new Set(matchedIds$1));
811
+ }
812
+ if (matchedSets.length === 0) return {
813
+ ...input,
814
+ filters: baseFilters
815
+ };
816
+ let matchedIds = matchedSets[0] ? [...matchedSets[0]] : [];
817
+ for (const set of matchedSets.slice(1)) matchedIds = matchedIds.filter((id) => set.has(id));
818
+ return {
819
+ ...input,
820
+ filters: [...baseFilters, {
821
+ id: idField,
822
+ value: matchedIds.length > 0 ? matchedIds : [NO_CRUD_EXTENSION_MATCH],
823
+ variant: "multiSelect",
824
+ operator: "inArray",
825
+ filterId: CRUD_EXTENSION_FILTER_ID
826
+ }]
827
+ };
828
+ }
612
829
  /**
613
830
  * 创建 CRUD Router
614
831
  *
@@ -624,14 +841,19 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
624
841
  function createCrudRouter(config) {
625
842
  const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
626
843
  const { selectSchema, schema, updateSchema } = resolveSchemas(config);
627
- const entityOutputSchema = selectSchema;
844
+ const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
628
845
  const listOutputSchema = zod.z.object({
629
- data: zod.z.array(selectSchema),
846
+ data: zod.z.array(entityOutputSchema),
630
847
  total: zod.z.number(),
631
848
  page: zod.z.number(),
632
849
  perPage: zod.z.number(),
633
850
  pageCount: zod.z.number()
634
851
  });
852
+ const metadataOutputSchema = zod.z.object({
853
+ schema: zod.z.unknown().optional(),
854
+ fields: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
855
+ errors: zod.z.array(zod.z.string()).optional()
856
+ });
635
857
  const deleteManyOutputSchema = zod.z.object({ deleted: zod.z.number() });
636
858
  const updateManyOutputSchema = zod.z.object({ updated: zod.z.number() });
637
859
  const upsertOutputSchema = zod.z.object({
@@ -639,12 +861,12 @@ function createCrudRouter(config) {
639
861
  isNew: zod.z.boolean()
640
862
  });
641
863
  const exportOutputSchema = zod.z.object({
642
- data: zod.z.array(selectSchema),
864
+ data: zod.z.array(entityOutputSchema),
643
865
  total: zod.z.number(),
644
866
  hasMore: zod.z.boolean()
645
867
  });
646
868
  const createManyOutputSchema = zod.z.object({
647
- created: zod.z.array(selectSchema),
869
+ created: zod.z.array(entityOutputSchema),
648
870
  count: zod.z.number()
649
871
  });
650
872
  const importOutputSchema = zod.z.object({
@@ -657,12 +879,14 @@ function createCrudRouter(config) {
657
879
  }))
658
880
  });
659
881
  const resolved = resolveConfig(config);
882
+ const metaProcedure = resolveMetaProcedure(config.procedure, resolved.procedureFactory("list"));
660
883
  const softDelete = resolveSoftDelete(softDeleteOption);
661
884
  const resolvedListInputSchema = config.listInputSchema ?? baseListInputSchema;
662
885
  const resolvedGetInputSchema = config.getInputSchema ? zod.z.union([zod.z.string(), config.getInputSchema]) : defaultGetInputSchema;
663
886
  const resolvedExportInputSchema = config.exportInputSchema ?? baseExportInputSchema;
664
887
  const m = middleware;
665
888
  const getIdColumn = () => table[idField];
889
+ const tableColumnNames = getTableColumnNames(table);
666
890
  const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
667
891
  const buildWhere = (ctx, operation, additionalCondition) => {
668
892
  const conditions = [];
@@ -702,25 +926,38 @@ function createCrudRouter(config) {
702
926
  });
703
927
  };
704
928
  const procedures = {
929
+ meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
930
+ await withGuard(ctx, "list");
931
+ const target = readCrudTarget(config);
932
+ if (!target) return {};
933
+ const provider = resolveCrudExtensions(ctx, config);
934
+ if (!provider?.getMetadata) return {};
935
+ return provider.getMetadata({ id: target.id });
936
+ }),
705
937
  list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
706
938
  await withGuard(ctx, "list");
707
939
  const doList = async (listInput$1) => {
708
- const offset = (listInput$1.page - 1) * listInput$1.perPage;
709
- const validatedFilters = listInput$1.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
940
+ const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, listInput$1);
941
+ const offset = (effectiveInput.page - 1) * effectiveInput.perPage;
942
+ const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
710
943
  const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
711
944
  table,
712
945
  filters: validatedFilters,
713
- joinOperator: listInput$1.joinOperator,
714
- resolveColumn: (columnId) => resolveColumnTarget({
715
- table,
716
- columnId: String(columnId),
717
- allowedColumns: filterableColumns
718
- })
946
+ joinOperator: effectiveInput.joinOperator,
947
+ resolveColumn: (columnId) => {
948
+ const id = String(columnId);
949
+ if (id === idField) return getTableColumn(table, idField);
950
+ return resolveColumnTarget({
951
+ table,
952
+ columnId: id,
953
+ allowedColumns: filterableColumns
954
+ });
955
+ }
719
956
  }) : void 0);
720
957
  let query = ctx.db.select().from(table).$dynamic();
721
958
  if (where) query = query.where(where);
722
- if (listInput$1.sort?.length) {
723
- const sortField = listInput$1.sort[0];
959
+ if (effectiveInput.sort?.length) {
960
+ const sortField = effectiveInput.sort[0];
724
961
  if (sortField && validateColumn(sortField.id, sortableColumns)) {
725
962
  const column = resolveColumnTarget({
726
963
  table,
@@ -730,16 +967,16 @@ function createCrudRouter(config) {
730
967
  if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
731
968
  }
732
969
  }
733
- const data = await query.limit(listInput$1.perPage).offset(offset);
970
+ const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveInput.perPage).offset(offset));
734
971
  let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
735
972
  if (where) countQuery = countQuery.where(where);
736
973
  const count = (await countQuery)[0]?.count ?? 0;
737
974
  return {
738
- data,
975
+ data: enrichedData,
739
976
  total: count,
740
- page: listInput$1.page,
741
- perPage: listInput$1.perPage,
742
- pageCount: Math.ceil(count / listInput$1.perPage)
977
+ page: effectiveInput.page,
978
+ perPage: effectiveInput.perPage,
979
+ pageCount: Math.ceil(count / effectiveInput.perPage)
743
980
  };
744
981
  };
745
982
  const listInput = input;
@@ -757,7 +994,9 @@ function createCrudRouter(config) {
757
994
  const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), id$1));
758
995
  const [item] = await ctx.db.select().from(table).where(where);
759
996
  await withAuthorize(ctx, item, "get");
760
- return item ?? null;
997
+ if (!item) return null;
998
+ const [enriched] = await enrichCrudRows(ctx, config, idField, [item]);
999
+ return enriched ?? null;
761
1000
  };
762
1001
  const id = typeof input === "string" ? input : input.id;
763
1002
  if (m.get) return m.get({
@@ -771,12 +1010,15 @@ function createCrudRouter(config) {
771
1010
  create: resolved.procedureFactory("create").input(schema).output(entityOutputSchema).mutation(async ({ ctx, input }) => {
772
1011
  await withGuard(ctx, "create");
773
1012
  const doCreate = async (inputData) => {
1013
+ const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
1014
+ assertCrudExtraValuesWritable(ctx, config, splitInput);
774
1015
  const injectData = resolved.getInject(ctx, "create");
775
1016
  const data = {
776
- ...inputData,
1017
+ ...splitInput.data,
777
1018
  ...injectData
778
1019
  };
779
1020
  const [created] = await ctx.db.insert(table).values(data).returning();
1021
+ await saveCrudExtraValues(ctx, config, created?.[idField], splitInput);
780
1022
  return created;
781
1023
  };
782
1024
  if (m.create) return m.create({
@@ -799,12 +1041,16 @@ function createCrudRouter(config) {
799
1041
  message: "Resource not found or access denied"
800
1042
  });
801
1043
  const doUpdate = async (updateData) => {
1044
+ const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
1045
+ assertCrudExtraValuesWritable(ctx, config, splitUpdate);
802
1046
  const injectData = resolved.getInject(ctx, "update");
803
1047
  const data = {
804
- ...updateData,
1048
+ ...splitUpdate.data,
805
1049
  ...injectData
806
1050
  };
807
- const [updated] = await ctx.db.update(table).set(data).where(where).returning();
1051
+ let updated = existing;
1052
+ if (Object.keys(data).length > 0) [updated] = await ctx.db.update(table).set(data).where(where).returning();
1053
+ await saveCrudExtraValues(ctx, config, input.id, splitUpdate);
808
1054
  return updated;
809
1055
  };
810
1056
  if (m.update) return m.update({
@@ -860,12 +1106,18 @@ function createCrudRouter(config) {
860
1106
  await withGuard(ctx, "updateMany");
861
1107
  const where = buildWhere(ctx, "updateMany", (0, drizzle_orm.inArray)(getIdColumn(), input.ids));
862
1108
  const doUpdateMany = async (updateData) => {
1109
+ const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
1110
+ assertCrudExtraValuesWritable(ctx, config, splitUpdate);
863
1111
  const injectData = resolved.getInject(ctx, "update");
864
1112
  const data = {
865
- ...updateData,
1113
+ ...splitUpdate.data,
866
1114
  ...injectData
867
1115
  };
868
- return { updated: (await ctx.db.update(table).set(data).where(where).returning({ id: getIdColumn() })).length };
1116
+ let updated = input.ids.map((id) => ({ [idField]: id }));
1117
+ if (Object.keys(data).length > 0) updated = await ctx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
1118
+ else updated = await ctx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
1119
+ await Promise.all(updated.map((row) => saveCrudExtraValues(ctx, config, row[idField], splitUpdate)));
1120
+ return { updated: updated.length };
869
1121
  };
870
1122
  if (m.updateMany) return m.updateMany({
871
1123
  ctx,
@@ -878,7 +1130,9 @@ function createCrudRouter(config) {
878
1130
  upsert: resolved.procedureFactory("upsert").input(schema).output(upsertOutputSchema).mutation(async ({ ctx, input }) => {
879
1131
  await withGuard(ctx, "upsert");
880
1132
  const doUpsert = async (inputData) => {
881
- const inputId = inputData[idField];
1133
+ const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
1134
+ assertCrudExtraValuesWritable(ctx, config, splitInput);
1135
+ const inputId = splitInput.data[idField];
882
1136
  let isNew = true;
883
1137
  let existing = null;
884
1138
  if (inputId) {
@@ -890,16 +1144,17 @@ function createCrudRouter(config) {
890
1144
  }
891
1145
  const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
892
1146
  const data = {
893
- ...inputData,
1147
+ ...splitInput.data,
894
1148
  ...injectData
895
1149
  };
896
1150
  const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
897
1151
  target: getIdColumn(),
898
1152
  set: {
899
- ...inputData,
1153
+ ...splitInput.data,
900
1154
  ...resolved.getInject(ctx, "update")
901
1155
  }
902
1156
  }).returning();
1157
+ await saveCrudExtraValues(ctx, config, result?.[idField] ?? inputId, splitInput);
903
1158
  return {
904
1159
  data: result,
905
1160
  isNew
@@ -915,22 +1170,32 @@ function createCrudRouter(config) {
915
1170
  export: resolved.procedureFactory("export").input(resolvedExportInputSchema).output(exportOutputSchema).query(async ({ ctx, input }) => {
916
1171
  await withGuard(ctx, "export");
917
1172
  const doExport = async (exportInput$1) => {
918
- const effectiveLimit = Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize);
919
- const validatedFilters = exportInput$1.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
1173
+ const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, {
1174
+ page: 1,
1175
+ perPage: Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize),
1176
+ joinOperator: exportInput$1.joinOperator ?? "and",
1177
+ ...exportInput$1
1178
+ });
1179
+ const effectiveLimit = Math.min(Math.max(effectiveInput.limit ?? maxExportSize, 1), maxExportSize);
1180
+ const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
920
1181
  const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
921
1182
  table,
922
1183
  filters: validatedFilters,
923
- joinOperator: exportInput$1.joinOperator ?? "and",
924
- resolveColumn: (columnId) => resolveColumnTarget({
925
- table,
926
- columnId: String(columnId),
927
- allowedColumns: filterableColumns
928
- })
1184
+ joinOperator: effectiveInput.joinOperator ?? "and",
1185
+ resolveColumn: (columnId) => {
1186
+ const id = String(columnId);
1187
+ if (id === idField) return getTableColumn(table, idField);
1188
+ return resolveColumnTarget({
1189
+ table,
1190
+ columnId: id,
1191
+ allowedColumns: filterableColumns
1192
+ });
1193
+ }
929
1194
  }) : void 0);
930
1195
  let query = ctx.db.select().from(table).$dynamic();
931
1196
  if (where) query = query.where(where);
932
- if (exportInput$1.sort?.length) {
933
- const sortField = exportInput$1.sort[0];
1197
+ if (effectiveInput.sort?.length) {
1198
+ const sortField = effectiveInput.sort[0];
934
1199
  if (sortField && validateColumn(sortField.id, sortableColumns)) {
935
1200
  const column = resolveColumnTarget({
936
1201
  table,
@@ -940,14 +1205,14 @@ function createCrudRouter(config) {
940
1205
  if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
941
1206
  }
942
1207
  }
943
- const data = await query.limit(effectiveLimit);
1208
+ const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveLimit));
944
1209
  let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
945
1210
  if (where) countQuery = countQuery.where(where);
946
1211
  const total = (await countQuery)[0]?.count ?? 0;
947
1212
  return {
948
- data,
1213
+ data: enrichedData,
949
1214
  total,
950
- hasMore: total > data.length
1215
+ hasMore: total > enrichedData.length
951
1216
  };
952
1217
  };
953
1218
  const exportInput = input;
@@ -961,12 +1226,15 @@ function createCrudRouter(config) {
961
1226
  createMany: resolved.procedureFactory("createMany").input(zod.z.array(schema).min(1).max(maxBatchSize)).output(createManyOutputSchema).mutation(async ({ ctx, input }) => {
962
1227
  await withGuard(ctx, "createMany");
963
1228
  const doCreateMany = async (items) => {
1229
+ const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
1230
+ assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
964
1231
  const injectData = resolved.getInject(ctx, "create");
965
- const values = items.map((item) => ({
966
- ...item,
1232
+ const values = splitItems.map(({ data }) => ({
1233
+ ...data,
967
1234
  ...injectData
968
1235
  }));
969
1236
  const created = await ctx.db.insert(table).values(values).onConflictDoNothing().returning();
1237
+ await saveCrudExtraValuesForRows(ctx, config, idField, created, splitItems);
970
1238
  return {
971
1239
  created,
972
1240
  count: created.length
@@ -987,8 +1255,10 @@ function createCrudRouter(config) {
987
1255
  for (let i = 0; i < importData$1.rows.length; i++) {
988
1256
  const row = importData$1.rows[i];
989
1257
  const result = schema.safeParse(row);
990
- if (result.success) validRows.push(result.data);
991
- else {
1258
+ if (result.success) {
1259
+ const splitRow = splitCrudExtensionWriteInput(result.data, tableColumnNames);
1260
+ validRows.push(splitRow);
1261
+ } else {
992
1262
  const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
993
1263
  failed.push({
994
1264
  row: i,
@@ -1008,26 +1278,30 @@ function createCrudRouter(config) {
1008
1278
  skipped: 0,
1009
1279
  failed
1010
1280
  };
1281
+ assertCrudExtraValuesWritableForRows(ctx, config, validRows);
1011
1282
  const injectData = resolved.getInject(ctx, "create");
1012
1283
  const values = validRows.map((row) => ({
1013
- ...row,
1284
+ ...row.data,
1014
1285
  ...injectData
1015
1286
  }));
1016
1287
  let insertedCount = 0;
1017
1288
  let updatedCount = 0;
1018
1289
  if (importData$1.onConflict === "upsert") {
1019
1290
  const existingCount = (await ctx.db.select({ id: getIdColumn() }).from(table).where((0, drizzle_orm.inArray)(getIdColumn(), values.map((v) => v[idField]).filter(Boolean)))).length;
1020
- const updateFields = Object.keys(validRows[0]);
1291
+ const updateFields = Object.keys(validRows[0].data);
1021
1292
  const setClause = { ...resolved.getInject(ctx, "update") };
1022
1293
  for (const key of updateFields) setClause[key] = drizzle_orm.sql.raw(`excluded."${key}"`);
1023
1294
  const results = await ctx.db.insert(table).values(values).onConflictDoUpdate({
1024
1295
  target: getIdColumn(),
1025
1296
  set: setClause
1026
1297
  }).returning({ id: getIdColumn() });
1298
+ await saveCrudExtraValuesForRows(ctx, config, idField, results.map((result) => ({ [idField]: result.id })), validRows);
1027
1299
  updatedCount = Math.min(existingCount, results.length);
1028
1300
  insertedCount = results.length - updatedCount;
1029
1301
  } else {
1030
- insertedCount = (await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() })).length;
1302
+ const inserted = await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() });
1303
+ await saveCrudExtraValuesForRows(ctx, config, idField, inserted.map((result) => ({ [idField]: result.id })), validRows);
1304
+ insertedCount = inserted.length;
1031
1305
  updatedCount = 0;
1032
1306
  }
1033
1307
  const skipped = validRows.length - insertedCount - updatedCount;