@wordrhyme/auto-crud-server 1.0.6 → 1.0.8

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
@@ -457,6 +457,7 @@ function isProcedureMap(config) {
457
457
  if (typeof config === "object" && config !== null) {
458
458
  const keys = Object.keys(config);
459
459
  const validKeys = [
460
+ "meta",
460
461
  "list",
461
462
  "get",
462
463
  "create",
@@ -502,6 +503,9 @@ function resolveConfig(config) {
502
503
  }
503
504
  };
504
505
  }
506
+ function resolveMetaProcedure(config, fallback) {
507
+ return isProcedureMap(config) ? config.meta ?? fallback : fallback;
508
+ }
505
509
  const filterItemSchema = zod.z.object({
506
510
  id: zod.z.string(),
507
511
  value: zod.z.union([zod.z.string(), zod.z.array(zod.z.string())]),
@@ -516,6 +520,7 @@ const baseListInputSchema = zod.z.object({
516
520
  id: zod.z.string(),
517
521
  desc: zod.z.boolean()
518
522
  })).optional(),
523
+ search: zod.z.string().trim().optional(),
519
524
  filters: zod.z.array(filterItemSchema).optional(),
520
525
  joinOperator: zod.z.enum(["and", "or"]).default("and")
521
526
  });
@@ -526,6 +531,7 @@ const baseExportInputSchema = zod.z.object({
526
531
  id: zod.z.string(),
527
532
  desc: zod.z.boolean()
528
533
  })).optional(),
534
+ search: zod.z.string().trim().optional(),
529
535
  filters: zod.z.array(filterItemSchema).optional(),
530
536
  joinOperator: zod.z.enum(["and", "or"]).default("and"),
531
537
  limit: zod.z.number().min(1).optional()
@@ -543,6 +549,13 @@ const DEFAULT_OMIT_FIELDS = [
543
549
  "createdAt",
544
550
  "updatedAt"
545
551
  ];
552
+ function shouldEnableCrudExtensionSchema(config) {
553
+ return Boolean(config.id) && config.extensions !== false;
554
+ }
555
+ function withCrudExtensionInput(schema, config) {
556
+ if (!shouldEnableCrudExtensionSchema(config) || !isZodObject(schema)) return schema;
557
+ return schema.passthrough();
558
+ }
546
559
  /**
547
560
  * 解析并派生 Schema
548
561
  * - 优先使用显式传入的 Schema
@@ -560,11 +573,12 @@ function resolveSchemas(config) {
560
573
  const omitConfig = Object.fromEntries(omitFields.map((f) => [f, true]));
561
574
  schema = rawSchema.omit(omitConfig);
562
575
  }
576
+ schema = withCrudExtensionInput(schema, config);
563
577
  let selectSchema;
564
578
  if (config.selectSchema) selectSchema = config.selectSchema;
565
579
  else if (config.schema) selectSchema = schema;
566
580
  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." });
581
+ 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
582
  return {
569
583
  selectSchema,
570
584
  schema,
@@ -609,6 +623,197 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
609
623
  }
610
624
  return getTableColumn(table, columnId);
611
625
  }
626
+ const CRUD_EXTENSION_FILTER_ID = "auto-crud-extension-filter";
627
+ const NO_CRUD_EXTENSION_MATCH = "__auto_crud_no_crud_extension_match__";
628
+ function isObjectRecord(value) {
629
+ return typeof value === "object" && value !== null && !Array.isArray(value);
630
+ }
631
+ function isCrudExtensionIdFilter(filter, idField) {
632
+ return filter.id === idField && filter.filterId === CRUD_EXTENSION_FILTER_ID;
633
+ }
634
+ function readCrudTarget(config) {
635
+ const id = config.id;
636
+ if (!id) return null;
637
+ return { id };
638
+ }
639
+ function resolveCrudExtensions(ctx, config) {
640
+ const extensions = config.extensions;
641
+ if (extensions === false) return null;
642
+ if (typeof extensions === "function") return extensions(ctx) ?? null;
643
+ if (extensions) return extensions;
644
+ if (!config.id) return null;
645
+ return ctx?.crudExtensions ?? null;
646
+ }
647
+ function getTableColumnNames(table) {
648
+ try {
649
+ const columns = (0, drizzle_orm.getTableColumns)(table);
650
+ const names = Object.keys(columns);
651
+ if (names.length > 0) return new Set(names);
652
+ } catch {}
653
+ return new Set(Object.keys(table).filter((key) => !key.startsWith("_")));
654
+ }
655
+ function splitCrudExtensionWriteInput(inputData, tableColumnNames) {
656
+ if (!isObjectRecord(inputData)) return {
657
+ data: inputData,
658
+ rawValues: {},
659
+ baseValues: {},
660
+ extraValues: null
661
+ };
662
+ const baseValues = {};
663
+ const extraValues = {};
664
+ for (const [key, value] of Object.entries(inputData)) {
665
+ if (key === "ext") {
666
+ if (isObjectRecord(value)) Object.assign(extraValues, value);
667
+ continue;
668
+ }
669
+ if (tableColumnNames.has(key)) baseValues[key] = value;
670
+ else extraValues[key] = value;
671
+ }
672
+ return {
673
+ data: baseValues,
674
+ rawValues: inputData,
675
+ baseValues,
676
+ extraValues: Object.keys(extraValues).length > 0 ? extraValues : null
677
+ };
678
+ }
679
+ async function saveCrudExtraValues(ctx, config, entityId, input) {
680
+ const target = readCrudTarget(config);
681
+ const extraValues = input.extraValues;
682
+ if (!target || !extraValues) return;
683
+ if (entityId === null || entityId === void 0 || String(entityId).length === 0) return;
684
+ const provider = resolveCrudExtensions(ctx, config);
685
+ if (!provider?.saveExtraValues) throw new __trpc_server.TRPCError({
686
+ code: "PRECONDITION_FAILED",
687
+ message: "CRUD extension persistence is not available"
688
+ });
689
+ await provider.saveExtraValues({
690
+ id: target.id,
691
+ entityId: String(entityId),
692
+ rawValues: input.rawValues,
693
+ baseValues: input.baseValues,
694
+ extraValues,
695
+ tx: ctx?.tx
696
+ });
697
+ }
698
+ function assertCrudExtraValuesWritable(ctx, config, input) {
699
+ if (!readCrudTarget(config) || !input.extraValues) return;
700
+ if (!resolveCrudExtensions(ctx, config)?.saveExtraValues) throw new __trpc_server.TRPCError({
701
+ code: "PRECONDITION_FAILED",
702
+ message: "CRUD extension persistence is not available"
703
+ });
704
+ }
705
+ function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
706
+ if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
707
+ }
708
+ async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
709
+ const extraByInputId = /* @__PURE__ */ new Map();
710
+ for (const input of inputs) {
711
+ if (!input.extraValues || !isObjectRecord(input.data)) continue;
712
+ const inputId = input.data[idField];
713
+ if (inputId !== null && inputId !== void 0) extraByInputId.set(String(inputId), input);
714
+ }
715
+ const canUseIndexFallback = results.length === inputs.length;
716
+ await Promise.all(results.map((result, index) => {
717
+ const resultId = result[idField];
718
+ const fallbackInput = canUseIndexFallback ? inputs[index] : void 0;
719
+ const fallbackInputId = fallbackInput && isObjectRecord(fallbackInput.data) ? fallbackInput.data[idField] : void 0;
720
+ const entityId = resultId !== null && resultId !== void 0 ? resultId : fallbackInputId;
721
+ const input = entityId !== null && entityId !== void 0 ? extraByInputId.get(String(entityId)) ?? fallbackInput : fallbackInput;
722
+ return input ? saveCrudExtraValues(ctx, config, entityId, input) : Promise.resolve();
723
+ }));
724
+ }
725
+ function readProjectionDisplay(value) {
726
+ if (!isObjectRecord(value)) return value;
727
+ if ("display" in value && value.display !== null && value.display !== void 0) return value.display;
728
+ if ("value" in value) return value.value;
729
+ return value;
730
+ }
731
+ async function enrichCrudRows(ctx, config, idField, rows) {
732
+ const target = readCrudTarget(config);
733
+ if (!target || rows.length === 0) return rows;
734
+ const provider = resolveCrudExtensions(ctx, config);
735
+ if (!provider?.readProjection) return rows;
736
+ const entityIds = rows.map((row) => row[idField]).filter((id) => typeof id === "string" && id.length > 0 || typeof id === "number").map(String);
737
+ if (entityIds.length === 0) return rows;
738
+ const projections = await provider.readProjection({
739
+ id: target.id,
740
+ entityIds
741
+ });
742
+ return rows.map((row) => {
743
+ const rowId = row[idField];
744
+ const projected = rowId !== null && rowId !== void 0 ? projections[String(rowId)] : void 0;
745
+ if (!projected || Object.keys(projected).length === 0) return row;
746
+ return {
747
+ ...row,
748
+ ...Object.fromEntries(Object.entries(projected).map(([field, value]) => [field, readProjectionDisplay(value)]))
749
+ };
750
+ });
751
+ }
752
+ function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
753
+ if (allowedColumns && allowedColumns.length > 0) return resolveColumnTarget({
754
+ table,
755
+ columnId,
756
+ allowedColumns
757
+ }) !== void 0;
758
+ return getTableColumn(table, columnId) !== void 0;
759
+ }
760
+ function isKnownBaseCrudColumn(table, columnId, allowedColumns) {
761
+ return getTableColumn(table, columnId) !== void 0 || findColumnRef(columnId, allowedColumns) !== void 0;
762
+ }
763
+ async function applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, input) {
764
+ const target = readCrudTarget(config);
765
+ const filters = input.filters ?? [];
766
+ const search = typeof input.search === "string" ? input.search.trim() : "";
767
+ if (!target || filters.length === 0 && !search) return input;
768
+ const baseFilters = [];
769
+ const extensionFilters = [];
770
+ for (const filter of filters) if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
771
+ else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
772
+ else extensionFilters.push(filter);
773
+ if (extensionFilters.length === 0 && !search) return {
774
+ ...input,
775
+ filters: baseFilters
776
+ };
777
+ const provider = resolveCrudExtensions(ctx, config);
778
+ const matchedSets = [];
779
+ if (extensionFilters.length > 0) {
780
+ if (!provider?.matchEntityIds) throw new __trpc_server.TRPCError({
781
+ code: "PRECONDITION_FAILED",
782
+ message: "CRUD extension filtering is not available"
783
+ });
784
+ const matchedIds$1 = await provider.matchEntityIds({
785
+ id: target.id,
786
+ filters: extensionFilters,
787
+ joinOperator: input.joinOperator,
788
+ limit: 5e3
789
+ });
790
+ matchedSets.push(new Set(matchedIds$1));
791
+ }
792
+ if (search && provider?.searchEntityIds) {
793
+ const matchedIds$1 = await provider.searchEntityIds({
794
+ id: target.id,
795
+ search,
796
+ limit: 5e3
797
+ });
798
+ matchedSets.push(new Set(matchedIds$1));
799
+ }
800
+ if (matchedSets.length === 0) return {
801
+ ...input,
802
+ filters: baseFilters
803
+ };
804
+ let matchedIds = matchedSets[0] ? [...matchedSets[0]] : [];
805
+ for (const set of matchedSets.slice(1)) matchedIds = matchedIds.filter((id) => set.has(id));
806
+ return {
807
+ ...input,
808
+ filters: [...baseFilters, {
809
+ id: idField,
810
+ value: matchedIds.length > 0 ? matchedIds : [NO_CRUD_EXTENSION_MATCH],
811
+ variant: "multiSelect",
812
+ operator: "inArray",
813
+ filterId: CRUD_EXTENSION_FILTER_ID
814
+ }]
815
+ };
816
+ }
612
817
  /**
613
818
  * 创建 CRUD Router
614
819
  *
@@ -624,14 +829,19 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
624
829
  function createCrudRouter(config) {
625
830
  const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
626
831
  const { selectSchema, schema, updateSchema } = resolveSchemas(config);
627
- const entityOutputSchema = selectSchema;
832
+ const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
628
833
  const listOutputSchema = zod.z.object({
629
- data: zod.z.array(selectSchema),
834
+ data: zod.z.array(entityOutputSchema),
630
835
  total: zod.z.number(),
631
836
  page: zod.z.number(),
632
837
  perPage: zod.z.number(),
633
838
  pageCount: zod.z.number()
634
839
  });
840
+ const metadataOutputSchema = zod.z.object({
841
+ schema: zod.z.unknown().optional(),
842
+ fields: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
843
+ errors: zod.z.array(zod.z.string()).optional()
844
+ });
635
845
  const deleteManyOutputSchema = zod.z.object({ deleted: zod.z.number() });
636
846
  const updateManyOutputSchema = zod.z.object({ updated: zod.z.number() });
637
847
  const upsertOutputSchema = zod.z.object({
@@ -639,12 +849,12 @@ function createCrudRouter(config) {
639
849
  isNew: zod.z.boolean()
640
850
  });
641
851
  const exportOutputSchema = zod.z.object({
642
- data: zod.z.array(selectSchema),
852
+ data: zod.z.array(entityOutputSchema),
643
853
  total: zod.z.number(),
644
854
  hasMore: zod.z.boolean()
645
855
  });
646
856
  const createManyOutputSchema = zod.z.object({
647
- created: zod.z.array(selectSchema),
857
+ created: zod.z.array(entityOutputSchema),
648
858
  count: zod.z.number()
649
859
  });
650
860
  const importOutputSchema = zod.z.object({
@@ -657,12 +867,14 @@ function createCrudRouter(config) {
657
867
  }))
658
868
  });
659
869
  const resolved = resolveConfig(config);
870
+ const metaProcedure = resolveMetaProcedure(config.procedure, resolved.procedureFactory("list"));
660
871
  const softDelete = resolveSoftDelete(softDeleteOption);
661
872
  const resolvedListInputSchema = config.listInputSchema ?? baseListInputSchema;
662
873
  const resolvedGetInputSchema = config.getInputSchema ? zod.z.union([zod.z.string(), config.getInputSchema]) : defaultGetInputSchema;
663
874
  const resolvedExportInputSchema = config.exportInputSchema ?? baseExportInputSchema;
664
875
  const m = middleware;
665
876
  const getIdColumn = () => table[idField];
877
+ const tableColumnNames = getTableColumnNames(table);
666
878
  const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
667
879
  const buildWhere = (ctx, operation, additionalCondition) => {
668
880
  const conditions = [];
@@ -702,25 +914,38 @@ function createCrudRouter(config) {
702
914
  });
703
915
  };
704
916
  const procedures = {
917
+ meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
918
+ await withGuard(ctx, "list");
919
+ const target = readCrudTarget(config);
920
+ if (!target) return {};
921
+ const provider = resolveCrudExtensions(ctx, config);
922
+ if (!provider?.getMetadata) return {};
923
+ return provider.getMetadata({ id: target.id });
924
+ }),
705
925
  list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
706
926
  await withGuard(ctx, "list");
707
927
  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));
928
+ const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, listInput$1);
929
+ const offset = (effectiveInput.page - 1) * effectiveInput.perPage;
930
+ const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
710
931
  const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
711
932
  table,
712
933
  filters: validatedFilters,
713
- joinOperator: listInput$1.joinOperator,
714
- resolveColumn: (columnId) => resolveColumnTarget({
715
- table,
716
- columnId: String(columnId),
717
- allowedColumns: filterableColumns
718
- })
934
+ joinOperator: effectiveInput.joinOperator,
935
+ resolveColumn: (columnId) => {
936
+ const id = String(columnId);
937
+ if (id === idField) return getTableColumn(table, idField);
938
+ return resolveColumnTarget({
939
+ table,
940
+ columnId: id,
941
+ allowedColumns: filterableColumns
942
+ });
943
+ }
719
944
  }) : void 0);
720
945
  let query = ctx.db.select().from(table).$dynamic();
721
946
  if (where) query = query.where(where);
722
- if (listInput$1.sort?.length) {
723
- const sortField = listInput$1.sort[0];
947
+ if (effectiveInput.sort?.length) {
948
+ const sortField = effectiveInput.sort[0];
724
949
  if (sortField && validateColumn(sortField.id, sortableColumns)) {
725
950
  const column = resolveColumnTarget({
726
951
  table,
@@ -730,16 +955,16 @@ function createCrudRouter(config) {
730
955
  if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
731
956
  }
732
957
  }
733
- const data = await query.limit(listInput$1.perPage).offset(offset);
958
+ const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveInput.perPage).offset(offset));
734
959
  let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
735
960
  if (where) countQuery = countQuery.where(where);
736
961
  const count = (await countQuery)[0]?.count ?? 0;
737
962
  return {
738
- data,
963
+ data: enrichedData,
739
964
  total: count,
740
- page: listInput$1.page,
741
- perPage: listInput$1.perPage,
742
- pageCount: Math.ceil(count / listInput$1.perPage)
965
+ page: effectiveInput.page,
966
+ perPage: effectiveInput.perPage,
967
+ pageCount: Math.ceil(count / effectiveInput.perPage)
743
968
  };
744
969
  };
745
970
  const listInput = input;
@@ -757,7 +982,9 @@ function createCrudRouter(config) {
757
982
  const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), id$1));
758
983
  const [item] = await ctx.db.select().from(table).where(where);
759
984
  await withAuthorize(ctx, item, "get");
760
- return item ?? null;
985
+ if (!item) return null;
986
+ const [enriched] = await enrichCrudRows(ctx, config, idField, [item]);
987
+ return enriched ?? null;
761
988
  };
762
989
  const id = typeof input === "string" ? input : input.id;
763
990
  if (m.get) return m.get({
@@ -771,12 +998,15 @@ function createCrudRouter(config) {
771
998
  create: resolved.procedureFactory("create").input(schema).output(entityOutputSchema).mutation(async ({ ctx, input }) => {
772
999
  await withGuard(ctx, "create");
773
1000
  const doCreate = async (inputData) => {
1001
+ const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
1002
+ assertCrudExtraValuesWritable(ctx, config, splitInput);
774
1003
  const injectData = resolved.getInject(ctx, "create");
775
1004
  const data = {
776
- ...inputData,
1005
+ ...splitInput.data,
777
1006
  ...injectData
778
1007
  };
779
1008
  const [created] = await ctx.db.insert(table).values(data).returning();
1009
+ await saveCrudExtraValues(ctx, config, created?.[idField], splitInput);
780
1010
  return created;
781
1011
  };
782
1012
  if (m.create) return m.create({
@@ -799,12 +1029,16 @@ function createCrudRouter(config) {
799
1029
  message: "Resource not found or access denied"
800
1030
  });
801
1031
  const doUpdate = async (updateData) => {
1032
+ const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
1033
+ assertCrudExtraValuesWritable(ctx, config, splitUpdate);
802
1034
  const injectData = resolved.getInject(ctx, "update");
803
1035
  const data = {
804
- ...updateData,
1036
+ ...splitUpdate.data,
805
1037
  ...injectData
806
1038
  };
807
- const [updated] = await ctx.db.update(table).set(data).where(where).returning();
1039
+ let updated = existing;
1040
+ if (Object.keys(data).length > 0) [updated] = await ctx.db.update(table).set(data).where(where).returning();
1041
+ await saveCrudExtraValues(ctx, config, input.id, splitUpdate);
808
1042
  return updated;
809
1043
  };
810
1044
  if (m.update) return m.update({
@@ -860,12 +1094,18 @@ function createCrudRouter(config) {
860
1094
  await withGuard(ctx, "updateMany");
861
1095
  const where = buildWhere(ctx, "updateMany", (0, drizzle_orm.inArray)(getIdColumn(), input.ids));
862
1096
  const doUpdateMany = async (updateData) => {
1097
+ const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
1098
+ assertCrudExtraValuesWritable(ctx, config, splitUpdate);
863
1099
  const injectData = resolved.getInject(ctx, "update");
864
1100
  const data = {
865
- ...updateData,
1101
+ ...splitUpdate.data,
866
1102
  ...injectData
867
1103
  };
868
- return { updated: (await ctx.db.update(table).set(data).where(where).returning({ id: getIdColumn() })).length };
1104
+ let updated = input.ids.map((id) => ({ [idField]: id }));
1105
+ if (Object.keys(data).length > 0) updated = await ctx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
1106
+ else updated = await ctx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
1107
+ await Promise.all(updated.map((row) => saveCrudExtraValues(ctx, config, row[idField], splitUpdate)));
1108
+ return { updated: updated.length };
869
1109
  };
870
1110
  if (m.updateMany) return m.updateMany({
871
1111
  ctx,
@@ -878,7 +1118,9 @@ function createCrudRouter(config) {
878
1118
  upsert: resolved.procedureFactory("upsert").input(schema).output(upsertOutputSchema).mutation(async ({ ctx, input }) => {
879
1119
  await withGuard(ctx, "upsert");
880
1120
  const doUpsert = async (inputData) => {
881
- const inputId = inputData[idField];
1121
+ const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
1122
+ assertCrudExtraValuesWritable(ctx, config, splitInput);
1123
+ const inputId = splitInput.data[idField];
882
1124
  let isNew = true;
883
1125
  let existing = null;
884
1126
  if (inputId) {
@@ -890,16 +1132,17 @@ function createCrudRouter(config) {
890
1132
  }
891
1133
  const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
892
1134
  const data = {
893
- ...inputData,
1135
+ ...splitInput.data,
894
1136
  ...injectData
895
1137
  };
896
1138
  const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
897
1139
  target: getIdColumn(),
898
1140
  set: {
899
- ...inputData,
1141
+ ...splitInput.data,
900
1142
  ...resolved.getInject(ctx, "update")
901
1143
  }
902
1144
  }).returning();
1145
+ await saveCrudExtraValues(ctx, config, result?.[idField] ?? inputId, splitInput);
903
1146
  return {
904
1147
  data: result,
905
1148
  isNew
@@ -915,22 +1158,32 @@ function createCrudRouter(config) {
915
1158
  export: resolved.procedureFactory("export").input(resolvedExportInputSchema).output(exportOutputSchema).query(async ({ ctx, input }) => {
916
1159
  await withGuard(ctx, "export");
917
1160
  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));
1161
+ const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, {
1162
+ page: 1,
1163
+ perPage: Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize),
1164
+ joinOperator: exportInput$1.joinOperator ?? "and",
1165
+ ...exportInput$1
1166
+ });
1167
+ const effectiveLimit = Math.min(Math.max(effectiveInput.limit ?? maxExportSize, 1), maxExportSize);
1168
+ const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
920
1169
  const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
921
1170
  table,
922
1171
  filters: validatedFilters,
923
- joinOperator: exportInput$1.joinOperator ?? "and",
924
- resolveColumn: (columnId) => resolveColumnTarget({
925
- table,
926
- columnId: String(columnId),
927
- allowedColumns: filterableColumns
928
- })
1172
+ joinOperator: effectiveInput.joinOperator ?? "and",
1173
+ resolveColumn: (columnId) => {
1174
+ const id = String(columnId);
1175
+ if (id === idField) return getTableColumn(table, idField);
1176
+ return resolveColumnTarget({
1177
+ table,
1178
+ columnId: id,
1179
+ allowedColumns: filterableColumns
1180
+ });
1181
+ }
929
1182
  }) : void 0);
930
1183
  let query = ctx.db.select().from(table).$dynamic();
931
1184
  if (where) query = query.where(where);
932
- if (exportInput$1.sort?.length) {
933
- const sortField = exportInput$1.sort[0];
1185
+ if (effectiveInput.sort?.length) {
1186
+ const sortField = effectiveInput.sort[0];
934
1187
  if (sortField && validateColumn(sortField.id, sortableColumns)) {
935
1188
  const column = resolveColumnTarget({
936
1189
  table,
@@ -940,14 +1193,14 @@ function createCrudRouter(config) {
940
1193
  if (column) query = query.orderBy(sortField.desc ? (0, drizzle_orm.desc)(column) : (0, drizzle_orm.asc)(column));
941
1194
  }
942
1195
  }
943
- const data = await query.limit(effectiveLimit);
1196
+ const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveLimit));
944
1197
  let countQuery = ctx.db.select({ count: drizzle_orm.sql`count(*)::int` }).from(table).$dynamic();
945
1198
  if (where) countQuery = countQuery.where(where);
946
1199
  const total = (await countQuery)[0]?.count ?? 0;
947
1200
  return {
948
- data,
1201
+ data: enrichedData,
949
1202
  total,
950
- hasMore: total > data.length
1203
+ hasMore: total > enrichedData.length
951
1204
  };
952
1205
  };
953
1206
  const exportInput = input;
@@ -961,12 +1214,15 @@ function createCrudRouter(config) {
961
1214
  createMany: resolved.procedureFactory("createMany").input(zod.z.array(schema).min(1).max(maxBatchSize)).output(createManyOutputSchema).mutation(async ({ ctx, input }) => {
962
1215
  await withGuard(ctx, "createMany");
963
1216
  const doCreateMany = async (items) => {
1217
+ const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
1218
+ assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
964
1219
  const injectData = resolved.getInject(ctx, "create");
965
- const values = items.map((item) => ({
966
- ...item,
1220
+ const values = splitItems.map(({ data }) => ({
1221
+ ...data,
967
1222
  ...injectData
968
1223
  }));
969
1224
  const created = await ctx.db.insert(table).values(values).onConflictDoNothing().returning();
1225
+ await saveCrudExtraValuesForRows(ctx, config, idField, created, splitItems);
970
1226
  return {
971
1227
  created,
972
1228
  count: created.length
@@ -987,8 +1243,10 @@ function createCrudRouter(config) {
987
1243
  for (let i = 0; i < importData$1.rows.length; i++) {
988
1244
  const row = importData$1.rows[i];
989
1245
  const result = schema.safeParse(row);
990
- if (result.success) validRows.push(result.data);
991
- else {
1246
+ if (result.success) {
1247
+ const splitRow = splitCrudExtensionWriteInput(result.data, tableColumnNames);
1248
+ validRows.push(splitRow);
1249
+ } else {
992
1250
  const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
993
1251
  failed.push({
994
1252
  row: i,
@@ -1008,26 +1266,30 @@ function createCrudRouter(config) {
1008
1266
  skipped: 0,
1009
1267
  failed
1010
1268
  };
1269
+ assertCrudExtraValuesWritableForRows(ctx, config, validRows);
1011
1270
  const injectData = resolved.getInject(ctx, "create");
1012
1271
  const values = validRows.map((row) => ({
1013
- ...row,
1272
+ ...row.data,
1014
1273
  ...injectData
1015
1274
  }));
1016
1275
  let insertedCount = 0;
1017
1276
  let updatedCount = 0;
1018
1277
  if (importData$1.onConflict === "upsert") {
1019
1278
  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]);
1279
+ const updateFields = Object.keys(validRows[0].data);
1021
1280
  const setClause = { ...resolved.getInject(ctx, "update") };
1022
1281
  for (const key of updateFields) setClause[key] = drizzle_orm.sql.raw(`excluded."${key}"`);
1023
1282
  const results = await ctx.db.insert(table).values(values).onConflictDoUpdate({
1024
1283
  target: getIdColumn(),
1025
1284
  set: setClause
1026
1285
  }).returning({ id: getIdColumn() });
1286
+ await saveCrudExtraValuesForRows(ctx, config, idField, results.map((result) => ({ [idField]: result.id })), validRows);
1027
1287
  updatedCount = Math.min(existingCount, results.length);
1028
1288
  insertedCount = results.length - updatedCount;
1029
1289
  } else {
1030
- insertedCount = (await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() })).length;
1290
+ const inserted = await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() });
1291
+ await saveCrudExtraValuesForRows(ctx, config, idField, inserted.map((result) => ({ [idField]: result.id })), validRows);
1292
+ insertedCount = inserted.length;
1031
1293
  updatedCount = 0;
1032
1294
  }
1033
1295
  const skipped = validRows.length - insertedCount - updatedCount;