@wordrhyme/auto-crud-server 0.2.0 → 0.3.0

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
@@ -373,9 +373,58 @@ const dataTableConfig = {
373
373
 
374
374
  //#endregion
375
375
  //#region src/routers/_factory.ts
376
- /**
377
- * 过滤器项 Schema
378
- */
376
+ function resolveSoftDelete(option) {
377
+ if (!option) return null;
378
+ if (option === true) return {
379
+ column: "deletedAt",
380
+ getValue: () => /* @__PURE__ */ new Date()
381
+ };
382
+ if (typeof option === "string") return {
383
+ column: option,
384
+ getValue: () => /* @__PURE__ */ new Date()
385
+ };
386
+ const config = option;
387
+ return {
388
+ column: config.column,
389
+ getValue: config.value ?? (() => /* @__PURE__ */ new Date())
390
+ };
391
+ }
392
+ function resolveConfig(config) {
393
+ const mode = config.mode || "declarative";
394
+ if (mode === "factory") {
395
+ const factoryConfig = config;
396
+ return {
397
+ procedureFactory: factoryConfig.procedureFactory,
398
+ guard: void 0,
399
+ getScope: () => void 0,
400
+ getInject: (ctx, op) => factoryConfig.inject?.(ctx, op) ?? {},
401
+ checkAuthorize: async () => true
402
+ };
403
+ }
404
+ if (mode === "procedures") {
405
+ const proceduresConfig = config;
406
+ const defaultProc = proceduresConfig.defaultProcedure ?? publicProcedure;
407
+ return {
408
+ procedureFactory: (op) => proceduresConfig.procedures[op] ?? defaultProc,
409
+ guard: void 0,
410
+ getScope: () => void 0,
411
+ getInject: (ctx, op) => proceduresConfig.inject?.(ctx, op) ?? {},
412
+ checkAuthorize: async () => true
413
+ };
414
+ }
415
+ const declarativeConfig = config;
416
+ const baseProcedure = declarativeConfig.procedure ?? publicProcedure;
417
+ return {
418
+ procedureFactory: () => baseProcedure,
419
+ guard: declarativeConfig.guard,
420
+ getScope: (ctx, tbl, op) => declarativeConfig.scope?.(ctx, tbl, op),
421
+ getInject: (ctx, op) => declarativeConfig.inject?.(ctx, op) ?? {},
422
+ checkAuthorize: async (ctx, resource, op) => {
423
+ if (declarativeConfig.authorize) return declarativeConfig.authorize(ctx, resource, op);
424
+ return true;
425
+ }
426
+ };
427
+ }
379
428
  const filterItemSchema = z.object({
380
429
  id: z.string(),
381
430
  value: z.union([z.string(), z.array(z.string())]),
@@ -383,9 +432,6 @@ const filterItemSchema = z.object({
383
432
  operator: z.enum(dataTableConfig.operators),
384
433
  filterId: z.string()
385
434
  });
386
- /**
387
- * 列表查询输入 Schema
388
- */
389
435
  const listInputSchema = z.object({
390
436
  page: z.number().min(1).default(1),
391
437
  perPage: z.number().min(1).max(100).default(10),
@@ -396,42 +442,53 @@ const listInputSchema = z.object({
396
442
  filters: z.array(filterItemSchema).optional(),
397
443
  joinOperator: z.enum(["and", "or"]).default("and")
398
444
  });
399
- /**
400
- * 验证列是否在白名单中
401
- */
402
- function validateColumn(columnId, allowedColumns, columnType) {
445
+ function validateColumn(columnId, allowedColumns) {
403
446
  if (!allowedColumns || allowedColumns.length === 0) return true;
404
447
  return allowedColumns.includes(columnId);
405
448
  }
406
- /**
407
- * 创建通用 CRUD Router
408
- */
409
449
  function createCrudRouter(config) {
410
- const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, procedure: baseProcedure = publicProcedure, authorize } = config;
450
+ const { table, insertSchema, updateSchema, idField = "id", filterableColumns, sortableColumns, maxBatchSize = 100, softDelete: softDeleteOption } = config;
451
+ const resolved = resolveConfig(config);
452
+ const softDelete = resolveSoftDelete(softDeleteOption);
411
453
  const getIdColumn = () => table[idField];
412
- const withAuthorize = async (operation, input) => {
413
- if (authorize) {
414
- if (!await authorize({
415
- operation,
416
- input
417
- })) throw new Error(`Unauthorized: ${operation} operation not allowed`);
454
+ const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
455
+ const buildWhere = (ctx, operation, additionalCondition) => {
456
+ const conditions = [];
457
+ const scopeCondition = resolved.getScope(ctx, table, operation);
458
+ if (scopeCondition) conditions.push(scopeCondition);
459
+ if (softDelete && (operation === "list" || operation === "get")) {
460
+ const col = getSoftDeleteColumn();
461
+ if (col) conditions.push(isNull(col));
418
462
  }
463
+ if (additionalCondition) conditions.push(additionalCondition);
464
+ if (conditions.length === 0) return void 0;
465
+ if (conditions.length === 1) return conditions[0];
466
+ return and(...conditions);
467
+ };
468
+ const withGuard = async (ctx, operation) => {
469
+ if (resolved.guard) {
470
+ if (!await resolved.guard(ctx, operation)) throw new Error(`Forbidden: ${operation} not allowed`);
471
+ }
472
+ };
473
+ const withAuthorize = async (ctx, resource, operation) => {
474
+ if (!resource) return;
475
+ if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new Error(`Forbidden: Cannot ${operation} this resource`);
419
476
  };
420
- return router({
421
- list: baseProcedure.input(listInputSchema).query(async ({ ctx, input }) => {
422
- await withAuthorize("list", input);
477
+ const procedures = {
478
+ list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
479
+ await withGuard(ctx, "list");
423
480
  const offset = (input.page - 1) * input.perPage;
424
- const validatedFilters = input.filters?.filter((filter) => validateColumn(filter.id, filterableColumns, "filter"));
425
- const where = validatedFilters?.length ? filterColumns({
481
+ const validatedFilters = input.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
482
+ const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
426
483
  table,
427
484
  filters: validatedFilters,
428
485
  joinOperator: input.joinOperator
429
- }) : void 0;
486
+ }) : void 0);
430
487
  let query = ctx.db.select().from(table).$dynamic();
431
488
  if (where) query = query.where(where);
432
489
  if (input.sort?.length) {
433
490
  const sortField = input.sort[0];
434
- if (sortField && validateColumn(sortField.id, sortableColumns, "sort")) {
491
+ if (sortField && validateColumn(sortField.id, sortableColumns)) {
435
492
  const column = table[sortField.id];
436
493
  if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
437
494
  }
@@ -448,46 +505,75 @@ function createCrudRouter(config) {
448
505
  pageCount: Math.ceil(count / input.perPage)
449
506
  };
450
507
  }),
451
- getById: baseProcedure.input(z.string()).query(async ({ ctx, input }) => {
452
- await withAuthorize("get", input);
453
- const column = getIdColumn();
454
- const [item] = await ctx.db.select().from(table).where(eq(column, input));
508
+ getById: resolved.procedureFactory("get").input(z.string()).query(async ({ ctx, input }) => {
509
+ await withGuard(ctx, "get");
510
+ const where = buildWhere(ctx, "get", eq(getIdColumn(), input));
511
+ const [item] = await ctx.db.select().from(table).where(where);
512
+ await withAuthorize(ctx, item, "get");
455
513
  return item ?? null;
456
514
  }),
457
- create: baseProcedure.input(insertSchema).mutation(async ({ ctx, input }) => {
458
- await withAuthorize("create", input);
459
- const [created] = await ctx.db.insert(table).values(input).returning();
515
+ create: resolved.procedureFactory("create").input(insertSchema).mutation(async ({ ctx, input }) => {
516
+ await withGuard(ctx, "create");
517
+ const injectData = resolved.getInject(ctx, "create");
518
+ const data = {
519
+ ...input,
520
+ ...injectData
521
+ };
522
+ const [created] = await ctx.db.insert(table).values(data).returning();
460
523
  return created;
461
524
  }),
462
- update: baseProcedure.input(z.object({
525
+ update: resolved.procedureFactory("update").input(z.object({
463
526
  id: z.string(),
464
527
  data: updateSchema
465
528
  })).mutation(async ({ ctx, input }) => {
466
- await withAuthorize("update", input);
467
- const column = getIdColumn();
468
- const [updated] = await ctx.db.update(table).set(input.data).where(eq(column, input.id)).returning();
529
+ await withGuard(ctx, "update");
530
+ const where = buildWhere(ctx, "update", eq(getIdColumn(), input.id));
531
+ const [existing] = await ctx.db.select().from(table).where(where);
532
+ await withAuthorize(ctx, existing, "update");
533
+ if (!existing) throw new Error("Resource not found or access denied");
534
+ const injectData = resolved.getInject(ctx, "update");
535
+ const data = {
536
+ ...input.data,
537
+ ...injectData
538
+ };
539
+ const [updated] = await ctx.db.update(table).set(data).where(where).returning();
469
540
  return updated;
470
541
  }),
471
- delete: baseProcedure.input(z.string()).mutation(async ({ ctx, input }) => {
472
- await withAuthorize("delete", input);
473
- const column = getIdColumn();
474
- const [deleted] = await ctx.db.delete(table).where(eq(column, input)).returning();
542
+ delete: resolved.procedureFactory("delete").input(z.string()).mutation(async ({ ctx, input }) => {
543
+ await withGuard(ctx, "delete");
544
+ const where = buildWhere(ctx, "delete", eq(getIdColumn(), input));
545
+ const [existing] = await ctx.db.select().from(table).where(where);
546
+ await withAuthorize(ctx, existing, "delete");
547
+ if (!existing) throw new Error("Resource not found or access denied");
548
+ if (softDelete) {
549
+ const [deleted$1] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
550
+ return deleted$1;
551
+ }
552
+ const [deleted] = await ctx.db.delete(table).where(where).returning();
475
553
  return deleted;
476
554
  }),
477
- deleteMany: baseProcedure.input(z.array(z.string()).max(maxBatchSize, `Maximum ${maxBatchSize} items allowed`)).mutation(async ({ ctx, input }) => {
478
- await withAuthorize("deleteMany", input);
479
- const column = getIdColumn();
480
- return { deleted: (await ctx.db.delete(table).where(inArray(column, input)).returning()).length };
555
+ deleteMany: resolved.procedureFactory("deleteMany").input(z.array(z.string()).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
556
+ await withGuard(ctx, "deleteMany");
557
+ const where = buildWhere(ctx, "deleteMany", inArray(getIdColumn(), input));
558
+ if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
559
+ return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
481
560
  }),
482
- updateMany: baseProcedure.input(z.object({
483
- ids: z.array(z.string()).max(maxBatchSize, `Maximum ${maxBatchSize} items allowed`),
561
+ updateMany: resolved.procedureFactory("updateMany").input(z.object({
562
+ ids: z.array(z.string()).max(maxBatchSize),
484
563
  data: updateSchema
485
564
  })).mutation(async ({ ctx, input }) => {
486
- await withAuthorize("updateMany", input);
487
- const column = getIdColumn();
488
- return { updated: (await ctx.db.update(table).set(input.data).where(inArray(column, input.ids)).returning()).length };
565
+ await withGuard(ctx, "updateMany");
566
+ const where = buildWhere(ctx, "updateMany", inArray(getIdColumn(), input.ids));
567
+ const injectData = resolved.getInject(ctx, "update");
568
+ const data = {
569
+ ...input.data,
570
+ ...injectData
571
+ };
572
+ return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
489
573
  })
490
- });
574
+ };
575
+ const crudRouter = router(procedures);
576
+ return Object.assign(crudRouter, { procedures });
491
577
  }
492
578
 
493
579
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wordrhyme/auto-crud-server",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
6
6
  "author": "wordrhyme",
7
7
  "license": "MIT",
@@ -46,11 +46,11 @@
46
46
  "tsdown": "^0.15.12",
47
47
  "typescript": "^5.9.3",
48
48
  "zod": "^4.1.13",
49
- "@internal/prettier-config": "0.0.1",
50
- "@internal/vitest-config": "0.1.0",
51
49
  "@internal/eslint-config": "0.3.0",
50
+ "@internal/prettier-config": "0.0.1",
51
+ "@internal/tsconfig": "0.1.0",
52
52
  "@internal/tsdown-config": "0.1.0",
53
- "@internal/tsconfig": "0.1.0"
53
+ "@internal/vitest-config": "0.1.0"
54
54
  },
55
55
  "prettier": "@internal/prettier-config",
56
56
  "scripts": {