@wordrhyme/auto-crud-server 1.0.7 → 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.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";
@@ -428,6 +428,7 @@ function isProcedureMap(config) {
428
428
  if (typeof config === "object" && config !== null) {
429
429
  const keys = Object.keys(config);
430
430
  const validKeys = [
431
+ "meta",
431
432
  "list",
432
433
  "get",
433
434
  "create",
@@ -473,6 +474,9 @@ function resolveConfig(config) {
473
474
  }
474
475
  };
475
476
  }
477
+ function resolveMetaProcedure(config, fallback) {
478
+ return isProcedureMap(config) ? config.meta ?? fallback : fallback;
479
+ }
476
480
  const filterItemSchema = z.object({
477
481
  id: z.string(),
478
482
  value: z.union([z.string(), z.array(z.string())]),
@@ -487,6 +491,7 @@ const baseListInputSchema = z.object({
487
491
  id: z.string(),
488
492
  desc: z.boolean()
489
493
  })).optional(),
494
+ search: z.string().trim().optional(),
490
495
  filters: z.array(filterItemSchema).optional(),
491
496
  joinOperator: z.enum(["and", "or"]).default("and")
492
497
  });
@@ -497,6 +502,7 @@ const baseExportInputSchema = z.object({
497
502
  id: z.string(),
498
503
  desc: z.boolean()
499
504
  })).optional(),
505
+ search: z.string().trim().optional(),
500
506
  filters: z.array(filterItemSchema).optional(),
501
507
  joinOperator: z.enum(["and", "or"]).default("and"),
502
508
  limit: z.number().min(1).optional()
@@ -514,6 +520,13 @@ const DEFAULT_OMIT_FIELDS = [
514
520
  "createdAt",
515
521
  "updatedAt"
516
522
  ];
523
+ function shouldEnableCrudExtensionSchema(config) {
524
+ return Boolean(config.id) && config.extensions !== false;
525
+ }
526
+ function withCrudExtensionInput(schema, config) {
527
+ if (!shouldEnableCrudExtensionSchema(config) || !isZodObject(schema)) return schema;
528
+ return schema.passthrough();
529
+ }
517
530
  /**
518
531
  * 解析并派生 Schema
519
532
  * - 优先使用显式传入的 Schema
@@ -531,11 +544,12 @@ function resolveSchemas(config) {
531
544
  const omitConfig = Object.fromEntries(omitFields.map((f) => [f, true]));
532
545
  schema = rawSchema.omit(omitConfig);
533
546
  }
547
+ schema = withCrudExtensionInput(schema, config);
534
548
  let selectSchema;
535
549
  if (config.selectSchema) selectSchema = config.selectSchema;
536
550
  else if (config.schema) selectSchema = schema;
537
551
  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." });
552
+ 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
553
  return {
540
554
  selectSchema,
541
555
  schema,
@@ -580,6 +594,197 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
580
594
  }
581
595
  return getTableColumn(table, columnId);
582
596
  }
597
+ const CRUD_EXTENSION_FILTER_ID = "auto-crud-extension-filter";
598
+ const NO_CRUD_EXTENSION_MATCH = "__auto_crud_no_crud_extension_match__";
599
+ function isObjectRecord(value) {
600
+ return typeof value === "object" && value !== null && !Array.isArray(value);
601
+ }
602
+ function isCrudExtensionIdFilter(filter, idField) {
603
+ return filter.id === idField && filter.filterId === CRUD_EXTENSION_FILTER_ID;
604
+ }
605
+ function readCrudTarget(config) {
606
+ const id = config.id;
607
+ if (!id) return null;
608
+ return { id };
609
+ }
610
+ function resolveCrudExtensions(ctx, config) {
611
+ const extensions = config.extensions;
612
+ if (extensions === false) return null;
613
+ if (typeof extensions === "function") return extensions(ctx) ?? null;
614
+ if (extensions) return extensions;
615
+ if (!config.id) return null;
616
+ return ctx?.crudExtensions ?? null;
617
+ }
618
+ function getTableColumnNames(table) {
619
+ try {
620
+ const columns = getTableColumns(table);
621
+ const names = Object.keys(columns);
622
+ if (names.length > 0) return new Set(names);
623
+ } catch {}
624
+ return new Set(Object.keys(table).filter((key) => !key.startsWith("_")));
625
+ }
626
+ function splitCrudExtensionWriteInput(inputData, tableColumnNames) {
627
+ if (!isObjectRecord(inputData)) return {
628
+ data: inputData,
629
+ rawValues: {},
630
+ baseValues: {},
631
+ extraValues: null
632
+ };
633
+ const baseValues = {};
634
+ const extraValues = {};
635
+ for (const [key, value] of Object.entries(inputData)) {
636
+ if (key === "ext") {
637
+ if (isObjectRecord(value)) Object.assign(extraValues, value);
638
+ continue;
639
+ }
640
+ if (tableColumnNames.has(key)) baseValues[key] = value;
641
+ else extraValues[key] = value;
642
+ }
643
+ return {
644
+ data: baseValues,
645
+ rawValues: inputData,
646
+ baseValues,
647
+ extraValues: Object.keys(extraValues).length > 0 ? extraValues : null
648
+ };
649
+ }
650
+ async function saveCrudExtraValues(ctx, config, entityId, input) {
651
+ const target = readCrudTarget(config);
652
+ const extraValues = input.extraValues;
653
+ if (!target || !extraValues) return;
654
+ if (entityId === null || entityId === void 0 || String(entityId).length === 0) return;
655
+ const provider = resolveCrudExtensions(ctx, config);
656
+ if (!provider?.saveExtraValues) throw new TRPCError({
657
+ code: "PRECONDITION_FAILED",
658
+ message: "CRUD extension persistence is not available"
659
+ });
660
+ await provider.saveExtraValues({
661
+ id: target.id,
662
+ entityId: String(entityId),
663
+ rawValues: input.rawValues,
664
+ baseValues: input.baseValues,
665
+ extraValues,
666
+ tx: ctx?.tx
667
+ });
668
+ }
669
+ function assertCrudExtraValuesWritable(ctx, config, input) {
670
+ if (!readCrudTarget(config) || !input.extraValues) return;
671
+ if (!resolveCrudExtensions(ctx, config)?.saveExtraValues) throw new TRPCError({
672
+ code: "PRECONDITION_FAILED",
673
+ message: "CRUD extension persistence is not available"
674
+ });
675
+ }
676
+ function assertCrudExtraValuesWritableForRows(ctx, config, inputs) {
677
+ if (inputs.some((input) => input.extraValues)) assertCrudExtraValuesWritable(ctx, config, inputs.find((input) => input.extraValues));
678
+ }
679
+ async function saveCrudExtraValuesForRows(ctx, config, idField, results, inputs) {
680
+ const extraByInputId = /* @__PURE__ */ new Map();
681
+ for (const input of inputs) {
682
+ if (!input.extraValues || !isObjectRecord(input.data)) continue;
683
+ const inputId = input.data[idField];
684
+ if (inputId !== null && inputId !== void 0) extraByInputId.set(String(inputId), input);
685
+ }
686
+ const canUseIndexFallback = results.length === inputs.length;
687
+ await Promise.all(results.map((result, index) => {
688
+ const resultId = result[idField];
689
+ const fallbackInput = canUseIndexFallback ? inputs[index] : void 0;
690
+ const fallbackInputId = fallbackInput && isObjectRecord(fallbackInput.data) ? fallbackInput.data[idField] : void 0;
691
+ const entityId = resultId !== null && resultId !== void 0 ? resultId : fallbackInputId;
692
+ const input = entityId !== null && entityId !== void 0 ? extraByInputId.get(String(entityId)) ?? fallbackInput : fallbackInput;
693
+ return input ? saveCrudExtraValues(ctx, config, entityId, input) : Promise.resolve();
694
+ }));
695
+ }
696
+ function readProjectionDisplay(value) {
697
+ if (!isObjectRecord(value)) return value;
698
+ if ("display" in value && value.display !== null && value.display !== void 0) return value.display;
699
+ if ("value" in value) return value.value;
700
+ return value;
701
+ }
702
+ async function enrichCrudRows(ctx, config, idField, rows) {
703
+ const target = readCrudTarget(config);
704
+ if (!target || rows.length === 0) return rows;
705
+ const provider = resolveCrudExtensions(ctx, config);
706
+ if (!provider?.readProjection) return rows;
707
+ const entityIds = rows.map((row) => row[idField]).filter((id) => typeof id === "string" && id.length > 0 || typeof id === "number").map(String);
708
+ if (entityIds.length === 0) return rows;
709
+ const projections = await provider.readProjection({
710
+ id: target.id,
711
+ entityIds
712
+ });
713
+ return rows.map((row) => {
714
+ const rowId = row[idField];
715
+ const projected = rowId !== null && rowId !== void 0 ? projections[String(rowId)] : void 0;
716
+ if (!projected || Object.keys(projected).length === 0) return row;
717
+ return {
718
+ ...row,
719
+ ...Object.fromEntries(Object.entries(projected).map(([field, value]) => [field, readProjectionDisplay(value)]))
720
+ };
721
+ });
722
+ }
723
+ function isAllowedBaseCrudColumn(table, columnId, allowedColumns) {
724
+ if (allowedColumns && allowedColumns.length > 0) return resolveColumnTarget({
725
+ table,
726
+ columnId,
727
+ allowedColumns
728
+ }) !== void 0;
729
+ return getTableColumn(table, columnId) !== void 0;
730
+ }
731
+ function isKnownBaseCrudColumn(table, columnId, allowedColumns) {
732
+ return getTableColumn(table, columnId) !== void 0 || findColumnRef(columnId, allowedColumns) !== void 0;
733
+ }
734
+ async function applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, input) {
735
+ const target = readCrudTarget(config);
736
+ const filters = input.filters ?? [];
737
+ const search = typeof input.search === "string" ? input.search.trim() : "";
738
+ if (!target || filters.length === 0 && !search) return input;
739
+ const baseFilters = [];
740
+ const extensionFilters = [];
741
+ for (const filter of filters) if (isAllowedBaseCrudColumn(table, filter.id, filterableColumns)) baseFilters.push(filter);
742
+ else if (isKnownBaseCrudColumn(table, filter.id, filterableColumns)) continue;
743
+ else extensionFilters.push(filter);
744
+ if (extensionFilters.length === 0 && !search) return {
745
+ ...input,
746
+ filters: baseFilters
747
+ };
748
+ const provider = resolveCrudExtensions(ctx, config);
749
+ const matchedSets = [];
750
+ if (extensionFilters.length > 0) {
751
+ if (!provider?.matchEntityIds) throw new TRPCError({
752
+ code: "PRECONDITION_FAILED",
753
+ message: "CRUD extension filtering is not available"
754
+ });
755
+ const matchedIds$1 = await provider.matchEntityIds({
756
+ id: target.id,
757
+ filters: extensionFilters,
758
+ joinOperator: input.joinOperator,
759
+ limit: 5e3
760
+ });
761
+ matchedSets.push(new Set(matchedIds$1));
762
+ }
763
+ if (search && provider?.searchEntityIds) {
764
+ const matchedIds$1 = await provider.searchEntityIds({
765
+ id: target.id,
766
+ search,
767
+ limit: 5e3
768
+ });
769
+ matchedSets.push(new Set(matchedIds$1));
770
+ }
771
+ if (matchedSets.length === 0) return {
772
+ ...input,
773
+ filters: baseFilters
774
+ };
775
+ let matchedIds = matchedSets[0] ? [...matchedSets[0]] : [];
776
+ for (const set of matchedSets.slice(1)) matchedIds = matchedIds.filter((id) => set.has(id));
777
+ return {
778
+ ...input,
779
+ filters: [...baseFilters, {
780
+ id: idField,
781
+ value: matchedIds.length > 0 ? matchedIds : [NO_CRUD_EXTENSION_MATCH],
782
+ variant: "multiSelect",
783
+ operator: "inArray",
784
+ filterId: CRUD_EXTENSION_FILTER_ID
785
+ }]
786
+ };
787
+ }
583
788
  /**
584
789
  * 创建 CRUD Router
585
790
  *
@@ -595,14 +800,19 @@ function resolveColumnTarget({ table, columnId, allowedColumns }) {
595
800
  function createCrudRouter(config) {
596
801
  const { table, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, maxExportSize = 5e3, softDelete: softDeleteOption, middleware = {} } = config;
597
802
  const { selectSchema, schema, updateSchema } = resolveSchemas(config);
598
- const entityOutputSchema = selectSchema;
803
+ const entityOutputSchema = withCrudExtensionInput(selectSchema, config);
599
804
  const listOutputSchema = z.object({
600
- data: z.array(selectSchema),
805
+ data: z.array(entityOutputSchema),
601
806
  total: z.number(),
602
807
  page: z.number(),
603
808
  perPage: z.number(),
604
809
  pageCount: z.number()
605
810
  });
811
+ const metadataOutputSchema = z.object({
812
+ schema: z.unknown().optional(),
813
+ fields: z.record(z.string(), z.unknown()).optional(),
814
+ errors: z.array(z.string()).optional()
815
+ });
606
816
  const deleteManyOutputSchema = z.object({ deleted: z.number() });
607
817
  const updateManyOutputSchema = z.object({ updated: z.number() });
608
818
  const upsertOutputSchema = z.object({
@@ -610,12 +820,12 @@ function createCrudRouter(config) {
610
820
  isNew: z.boolean()
611
821
  });
612
822
  const exportOutputSchema = z.object({
613
- data: z.array(selectSchema),
823
+ data: z.array(entityOutputSchema),
614
824
  total: z.number(),
615
825
  hasMore: z.boolean()
616
826
  });
617
827
  const createManyOutputSchema = z.object({
618
- created: z.array(selectSchema),
828
+ created: z.array(entityOutputSchema),
619
829
  count: z.number()
620
830
  });
621
831
  const importOutputSchema = z.object({
@@ -628,12 +838,14 @@ function createCrudRouter(config) {
628
838
  }))
629
839
  });
630
840
  const resolved = resolveConfig(config);
841
+ const metaProcedure = resolveMetaProcedure(config.procedure, resolved.procedureFactory("list"));
631
842
  const softDelete = resolveSoftDelete(softDeleteOption);
632
843
  const resolvedListInputSchema = config.listInputSchema ?? baseListInputSchema;
633
844
  const resolvedGetInputSchema = config.getInputSchema ? z.union([z.string(), config.getInputSchema]) : defaultGetInputSchema;
634
845
  const resolvedExportInputSchema = config.exportInputSchema ?? baseExportInputSchema;
635
846
  const m = middleware;
636
847
  const getIdColumn = () => table[idField];
848
+ const tableColumnNames = getTableColumnNames(table);
637
849
  const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
638
850
  const buildWhere = (ctx, operation, additionalCondition) => {
639
851
  const conditions = [];
@@ -673,25 +885,38 @@ function createCrudRouter(config) {
673
885
  });
674
886
  };
675
887
  const procedures = {
888
+ meta: metaProcedure.output(metadataOutputSchema).query(async ({ ctx }) => {
889
+ await withGuard(ctx, "list");
890
+ const target = readCrudTarget(config);
891
+ if (!target) return {};
892
+ const provider = resolveCrudExtensions(ctx, config);
893
+ if (!provider?.getMetadata) return {};
894
+ return provider.getMetadata({ id: target.id });
895
+ }),
676
896
  list: resolved.procedureFactory("list").input(resolvedListInputSchema).output(listOutputSchema).query(async ({ ctx, input }) => {
677
897
  await withGuard(ctx, "list");
678
898
  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));
899
+ const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, listInput$1);
900
+ const offset = (effectiveInput.page - 1) * effectiveInput.perPage;
901
+ const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
681
902
  const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
682
903
  table,
683
904
  filters: validatedFilters,
684
- joinOperator: listInput$1.joinOperator,
685
- resolveColumn: (columnId) => resolveColumnTarget({
686
- table,
687
- columnId: String(columnId),
688
- allowedColumns: filterableColumns
689
- })
905
+ joinOperator: effectiveInput.joinOperator,
906
+ resolveColumn: (columnId) => {
907
+ const id = String(columnId);
908
+ if (id === idField) return getTableColumn(table, idField);
909
+ return resolveColumnTarget({
910
+ table,
911
+ columnId: id,
912
+ allowedColumns: filterableColumns
913
+ });
914
+ }
690
915
  }) : void 0);
691
916
  let query = ctx.db.select().from(table).$dynamic();
692
917
  if (where) query = query.where(where);
693
- if (listInput$1.sort?.length) {
694
- const sortField = listInput$1.sort[0];
918
+ if (effectiveInput.sort?.length) {
919
+ const sortField = effectiveInput.sort[0];
695
920
  if (sortField && validateColumn(sortField.id, sortableColumns)) {
696
921
  const column = resolveColumnTarget({
697
922
  table,
@@ -701,16 +926,16 @@ function createCrudRouter(config) {
701
926
  if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
702
927
  }
703
928
  }
704
- const data = await query.limit(listInput$1.perPage).offset(offset);
929
+ const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveInput.perPage).offset(offset));
705
930
  let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
706
931
  if (where) countQuery = countQuery.where(where);
707
932
  const count = (await countQuery)[0]?.count ?? 0;
708
933
  return {
709
- data,
934
+ data: enrichedData,
710
935
  total: count,
711
- page: listInput$1.page,
712
- perPage: listInput$1.perPage,
713
- pageCount: Math.ceil(count / listInput$1.perPage)
936
+ page: effectiveInput.page,
937
+ perPage: effectiveInput.perPage,
938
+ pageCount: Math.ceil(count / effectiveInput.perPage)
714
939
  };
715
940
  };
716
941
  const listInput = input;
@@ -728,7 +953,9 @@ function createCrudRouter(config) {
728
953
  const where = buildWhere(ctx, "get", eq(getIdColumn(), id$1));
729
954
  const [item] = await ctx.db.select().from(table).where(where);
730
955
  await withAuthorize(ctx, item, "get");
731
- return item ?? null;
956
+ if (!item) return null;
957
+ const [enriched] = await enrichCrudRows(ctx, config, idField, [item]);
958
+ return enriched ?? null;
732
959
  };
733
960
  const id = typeof input === "string" ? input : input.id;
734
961
  if (m.get) return m.get({
@@ -742,12 +969,15 @@ function createCrudRouter(config) {
742
969
  create: resolved.procedureFactory("create").input(schema).output(entityOutputSchema).mutation(async ({ ctx, input }) => {
743
970
  await withGuard(ctx, "create");
744
971
  const doCreate = async (inputData) => {
972
+ const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
973
+ assertCrudExtraValuesWritable(ctx, config, splitInput);
745
974
  const injectData = resolved.getInject(ctx, "create");
746
975
  const data = {
747
- ...inputData,
976
+ ...splitInput.data,
748
977
  ...injectData
749
978
  };
750
979
  const [created] = await ctx.db.insert(table).values(data).returning();
980
+ await saveCrudExtraValues(ctx, config, created?.[idField], splitInput);
751
981
  return created;
752
982
  };
753
983
  if (m.create) return m.create({
@@ -770,12 +1000,16 @@ function createCrudRouter(config) {
770
1000
  message: "Resource not found or access denied"
771
1001
  });
772
1002
  const doUpdate = async (updateData) => {
1003
+ const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
1004
+ assertCrudExtraValuesWritable(ctx, config, splitUpdate);
773
1005
  const injectData = resolved.getInject(ctx, "update");
774
1006
  const data = {
775
- ...updateData,
1007
+ ...splitUpdate.data,
776
1008
  ...injectData
777
1009
  };
778
- const [updated] = await ctx.db.update(table).set(data).where(where).returning();
1010
+ let updated = existing;
1011
+ if (Object.keys(data).length > 0) [updated] = await ctx.db.update(table).set(data).where(where).returning();
1012
+ await saveCrudExtraValues(ctx, config, input.id, splitUpdate);
779
1013
  return updated;
780
1014
  };
781
1015
  if (m.update) return m.update({
@@ -831,12 +1065,18 @@ function createCrudRouter(config) {
831
1065
  await withGuard(ctx, "updateMany");
832
1066
  const where = buildWhere(ctx, "updateMany", inArray(getIdColumn(), input.ids));
833
1067
  const doUpdateMany = async (updateData) => {
1068
+ const splitUpdate = splitCrudExtensionWriteInput(updateData, tableColumnNames);
1069
+ assertCrudExtraValuesWritable(ctx, config, splitUpdate);
834
1070
  const injectData = resolved.getInject(ctx, "update");
835
1071
  const data = {
836
- ...updateData,
1072
+ ...splitUpdate.data,
837
1073
  ...injectData
838
1074
  };
839
- return { updated: (await ctx.db.update(table).set(data).where(where).returning({ id: getIdColumn() })).length };
1075
+ let updated = input.ids.map((id) => ({ [idField]: id }));
1076
+ if (Object.keys(data).length > 0) updated = await ctx.db.update(table).set(data).where(where).returning({ [idField]: getIdColumn() });
1077
+ else updated = await ctx.db.select({ [idField]: getIdColumn() }).from(table).where(where);
1078
+ await Promise.all(updated.map((row) => saveCrudExtraValues(ctx, config, row[idField], splitUpdate)));
1079
+ return { updated: updated.length };
840
1080
  };
841
1081
  if (m.updateMany) return m.updateMany({
842
1082
  ctx,
@@ -849,7 +1089,9 @@ function createCrudRouter(config) {
849
1089
  upsert: resolved.procedureFactory("upsert").input(schema).output(upsertOutputSchema).mutation(async ({ ctx, input }) => {
850
1090
  await withGuard(ctx, "upsert");
851
1091
  const doUpsert = async (inputData) => {
852
- const inputId = inputData[idField];
1092
+ const splitInput = splitCrudExtensionWriteInput(inputData, tableColumnNames);
1093
+ assertCrudExtraValuesWritable(ctx, config, splitInput);
1094
+ const inputId = splitInput.data[idField];
853
1095
  let isNew = true;
854
1096
  let existing = null;
855
1097
  if (inputId) {
@@ -861,16 +1103,17 @@ function createCrudRouter(config) {
861
1103
  }
862
1104
  const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
863
1105
  const data = {
864
- ...inputData,
1106
+ ...splitInput.data,
865
1107
  ...injectData
866
1108
  };
867
1109
  const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
868
1110
  target: getIdColumn(),
869
1111
  set: {
870
- ...inputData,
1112
+ ...splitInput.data,
871
1113
  ...resolved.getInject(ctx, "update")
872
1114
  }
873
1115
  }).returning();
1116
+ await saveCrudExtraValues(ctx, config, result?.[idField] ?? inputId, splitInput);
874
1117
  return {
875
1118
  data: result,
876
1119
  isNew
@@ -886,22 +1129,32 @@ function createCrudRouter(config) {
886
1129
  export: resolved.procedureFactory("export").input(resolvedExportInputSchema).output(exportOutputSchema).query(async ({ ctx, input }) => {
887
1130
  await withGuard(ctx, "export");
888
1131
  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));
1132
+ const effectiveInput = await applyCrudExtensionFilters(ctx, config, table, idField, filterableColumns, {
1133
+ page: 1,
1134
+ perPage: Math.min(Math.max(exportInput$1.limit ?? maxExportSize, 1), maxExportSize),
1135
+ joinOperator: exportInput$1.joinOperator ?? "and",
1136
+ ...exportInput$1
1137
+ });
1138
+ const effectiveLimit = Math.min(Math.max(effectiveInput.limit ?? maxExportSize, 1), maxExportSize);
1139
+ const validatedFilters = effectiveInput.filters?.filter((filter) => isCrudExtensionIdFilter(filter, idField) || validateColumn(filter.id, filterableColumns));
891
1140
  const where = buildWhere(ctx, "export", validatedFilters?.length ? filterColumns({
892
1141
  table,
893
1142
  filters: validatedFilters,
894
- joinOperator: exportInput$1.joinOperator ?? "and",
895
- resolveColumn: (columnId) => resolveColumnTarget({
896
- table,
897
- columnId: String(columnId),
898
- allowedColumns: filterableColumns
899
- })
1143
+ joinOperator: effectiveInput.joinOperator ?? "and",
1144
+ resolveColumn: (columnId) => {
1145
+ const id = String(columnId);
1146
+ if (id === idField) return getTableColumn(table, idField);
1147
+ return resolveColumnTarget({
1148
+ table,
1149
+ columnId: id,
1150
+ allowedColumns: filterableColumns
1151
+ });
1152
+ }
900
1153
  }) : void 0);
901
1154
  let query = ctx.db.select().from(table).$dynamic();
902
1155
  if (where) query = query.where(where);
903
- if (exportInput$1.sort?.length) {
904
- const sortField = exportInput$1.sort[0];
1156
+ if (effectiveInput.sort?.length) {
1157
+ const sortField = effectiveInput.sort[0];
905
1158
  if (sortField && validateColumn(sortField.id, sortableColumns)) {
906
1159
  const column = resolveColumnTarget({
907
1160
  table,
@@ -911,14 +1164,14 @@ function createCrudRouter(config) {
911
1164
  if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
912
1165
  }
913
1166
  }
914
- const data = await query.limit(effectiveLimit);
1167
+ const enrichedData = await enrichCrudRows(ctx, config, idField, await query.limit(effectiveLimit));
915
1168
  let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
916
1169
  if (where) countQuery = countQuery.where(where);
917
1170
  const total = (await countQuery)[0]?.count ?? 0;
918
1171
  return {
919
- data,
1172
+ data: enrichedData,
920
1173
  total,
921
- hasMore: total > data.length
1174
+ hasMore: total > enrichedData.length
922
1175
  };
923
1176
  };
924
1177
  const exportInput = input;
@@ -932,12 +1185,15 @@ function createCrudRouter(config) {
932
1185
  createMany: resolved.procedureFactory("createMany").input(z.array(schema).min(1).max(maxBatchSize)).output(createManyOutputSchema).mutation(async ({ ctx, input }) => {
933
1186
  await withGuard(ctx, "createMany");
934
1187
  const doCreateMany = async (items) => {
1188
+ const splitItems = items.map((item) => splitCrudExtensionWriteInput(item, tableColumnNames));
1189
+ assertCrudExtraValuesWritableForRows(ctx, config, splitItems);
935
1190
  const injectData = resolved.getInject(ctx, "create");
936
- const values = items.map((item) => ({
937
- ...item,
1191
+ const values = splitItems.map(({ data }) => ({
1192
+ ...data,
938
1193
  ...injectData
939
1194
  }));
940
1195
  const created = await ctx.db.insert(table).values(values).onConflictDoNothing().returning();
1196
+ await saveCrudExtraValuesForRows(ctx, config, idField, created, splitItems);
941
1197
  return {
942
1198
  created,
943
1199
  count: created.length
@@ -958,8 +1214,10 @@ function createCrudRouter(config) {
958
1214
  for (let i = 0; i < importData$1.rows.length; i++) {
959
1215
  const row = importData$1.rows[i];
960
1216
  const result = schema.safeParse(row);
961
- if (result.success) validRows.push(result.data);
962
- else {
1217
+ if (result.success) {
1218
+ const splitRow = splitCrudExtensionWriteInput(result.data, tableColumnNames);
1219
+ validRows.push(splitRow);
1220
+ } else {
963
1221
  const errors = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`);
964
1222
  failed.push({
965
1223
  row: i,
@@ -979,26 +1237,30 @@ function createCrudRouter(config) {
979
1237
  skipped: 0,
980
1238
  failed
981
1239
  };
1240
+ assertCrudExtraValuesWritableForRows(ctx, config, validRows);
982
1241
  const injectData = resolved.getInject(ctx, "create");
983
1242
  const values = validRows.map((row) => ({
984
- ...row,
1243
+ ...row.data,
985
1244
  ...injectData
986
1245
  }));
987
1246
  let insertedCount = 0;
988
1247
  let updatedCount = 0;
989
1248
  if (importData$1.onConflict === "upsert") {
990
1249
  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]);
1250
+ const updateFields = Object.keys(validRows[0].data);
992
1251
  const setClause = { ...resolved.getInject(ctx, "update") };
993
1252
  for (const key of updateFields) setClause[key] = sql.raw(`excluded."${key}"`);
994
1253
  const results = await ctx.db.insert(table).values(values).onConflictDoUpdate({
995
1254
  target: getIdColumn(),
996
1255
  set: setClause
997
1256
  }).returning({ id: getIdColumn() });
1257
+ await saveCrudExtraValuesForRows(ctx, config, idField, results.map((result) => ({ [idField]: result.id })), validRows);
998
1258
  updatedCount = Math.min(existingCount, results.length);
999
1259
  insertedCount = results.length - updatedCount;
1000
1260
  } else {
1001
- insertedCount = (await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() })).length;
1261
+ const inserted = await ctx.db.insert(table).values(values).onConflictDoNothing().returning({ id: getIdColumn() });
1262
+ await saveCrudExtraValuesForRows(ctx, config, idField, inserted.map((result) => ({ [idField]: result.id })), validRows);
1263
+ insertedCount = inserted.length;
1002
1264
  updatedCount = 0;
1003
1265
  }
1004
1266
  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.8",
5
5
  "description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
6
6
  "author": "wordrhyme",
7
7
  "license": "MIT",
@@ -38,18 +38,18 @@
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",
52
+ "@internal/tsconfig": "0.1.0",
53
53
  "@internal/tsdown-config": "0.1.0",
54
54
  "@internal/vitest-config": "0.1.0"
55
55
  },