@wordrhyme/auto-crud-server 0.2.0 → 0.4.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,98 +442,178 @@ 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, hooks = {} } = config;
451
+ const resolved = resolveConfig(config);
452
+ const softDelete = resolveSoftDelete(softDeleteOption);
453
+ const h = hooks;
411
454
  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`);
455
+ const getSoftDeleteColumn = () => softDelete ? table[softDelete.column] : null;
456
+ const buildWhere = (ctx, operation, additionalCondition) => {
457
+ const conditions = [];
458
+ const scopeCondition = resolved.getScope(ctx, table, operation);
459
+ if (scopeCondition) conditions.push(scopeCondition);
460
+ if (softDelete && (operation === "list" || operation === "get")) {
461
+ const col = getSoftDeleteColumn();
462
+ if (col) conditions.push(isNull(col));
463
+ }
464
+ if (additionalCondition) conditions.push(additionalCondition);
465
+ if (conditions.length === 0) return void 0;
466
+ if (conditions.length === 1) return conditions[0];
467
+ return and(...conditions);
468
+ };
469
+ const withGuard = async (ctx, operation) => {
470
+ if (resolved.guard) {
471
+ if (!await resolved.guard(ctx, operation)) throw new Error(`Forbidden: ${operation} not allowed`);
418
472
  }
419
473
  };
420
- return router({
421
- list: baseProcedure.input(listInputSchema).query(async ({ ctx, input }) => {
422
- await withAuthorize("list", input);
423
- 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({
474
+ const withAuthorize = async (ctx, resource, operation) => {
475
+ if (!resource) return;
476
+ if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new Error(`Forbidden: Cannot ${operation} this resource`);
477
+ };
478
+ const procedures = {
479
+ list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
480
+ await withGuard(ctx, "list");
481
+ let processedInput = input;
482
+ if (h.beforeList) {
483
+ const result = await h.beforeList(ctx, processedInput);
484
+ if (result) processedInput = result;
485
+ }
486
+ const offset = (processedInput.page - 1) * processedInput.perPage;
487
+ const validatedFilters = processedInput.filters?.filter((filter) => validateColumn(filter.id, filterableColumns));
488
+ const where = buildWhere(ctx, "list", validatedFilters?.length ? filterColumns({
426
489
  table,
427
490
  filters: validatedFilters,
428
- joinOperator: input.joinOperator
429
- }) : void 0;
491
+ joinOperator: processedInput.joinOperator
492
+ }) : void 0);
430
493
  let query = ctx.db.select().from(table).$dynamic();
431
494
  if (where) query = query.where(where);
432
- if (input.sort?.length) {
433
- const sortField = input.sort[0];
434
- if (sortField && validateColumn(sortField.id, sortableColumns, "sort")) {
495
+ if (processedInput.sort?.length) {
496
+ const sortField = processedInput.sort[0];
497
+ if (sortField && validateColumn(sortField.id, sortableColumns)) {
435
498
  const column = table[sortField.id];
436
499
  if (column) query = query.orderBy(sortField.desc ? desc(column) : asc(column));
437
500
  }
438
501
  }
439
- const data = await query.limit(input.perPage).offset(offset);
502
+ const data = await query.limit(processedInput.perPage).offset(offset);
440
503
  let countQuery = ctx.db.select({ count: sql`count(*)::int` }).from(table).$dynamic();
441
504
  if (where) countQuery = countQuery.where(where);
442
505
  const count = (await countQuery)[0]?.count ?? 0;
443
- return {
506
+ let listResult = {
444
507
  data,
445
508
  total: count,
446
- page: input.page,
447
- perPage: input.perPage,
448
- pageCount: Math.ceil(count / input.perPage)
509
+ page: processedInput.page,
510
+ perPage: processedInput.perPage,
511
+ pageCount: Math.ceil(count / processedInput.perPage)
449
512
  };
513
+ if (h.afterList) {
514
+ const result = await h.afterList(ctx, listResult);
515
+ if (result) listResult = result;
516
+ }
517
+ return listResult;
450
518
  }),
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));
455
- return item ?? null;
519
+ getById: resolved.procedureFactory("get").input(z.string()).query(async ({ ctx, input }) => {
520
+ await withGuard(ctx, "get");
521
+ if (h.beforeGet) await h.beforeGet(ctx, input);
522
+ const where = buildWhere(ctx, "get", eq(getIdColumn(), input));
523
+ const [item] = await ctx.db.select().from(table).where(where);
524
+ await withAuthorize(ctx, item, "get");
525
+ let result = item ?? null;
526
+ if (h.afterGet) {
527
+ const hookResult = await h.afterGet(ctx, result);
528
+ if (hookResult !== void 0) result = hookResult;
529
+ }
530
+ return result;
456
531
  }),
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();
532
+ create: resolved.procedureFactory("create").input(insertSchema).mutation(async ({ ctx, input }) => {
533
+ await withGuard(ctx, "create");
534
+ let processedData = input;
535
+ if (h.beforeCreate) {
536
+ const result = await h.beforeCreate(ctx, processedData);
537
+ if (result) processedData = result;
538
+ }
539
+ const injectData = resolved.getInject(ctx, "create");
540
+ const data = {
541
+ ...processedData,
542
+ ...injectData
543
+ };
544
+ const [created] = await ctx.db.insert(table).values(data).returning();
545
+ if (h.afterCreate) await h.afterCreate(ctx, created);
460
546
  return created;
461
547
  }),
462
- update: baseProcedure.input(z.object({
548
+ update: resolved.procedureFactory("update").input(z.object({
463
549
  id: z.string(),
464
550
  data: updateSchema
465
551
  })).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();
552
+ await withGuard(ctx, "update");
553
+ const where = buildWhere(ctx, "update", eq(getIdColumn(), input.id));
554
+ const [existing] = await ctx.db.select().from(table).where(where);
555
+ await withAuthorize(ctx, existing, "update");
556
+ if (!existing) throw new Error("Resource not found or access denied");
557
+ let processedData = input.data;
558
+ if (h.beforeUpdate) {
559
+ const result = await h.beforeUpdate(ctx, input.id, processedData, existing);
560
+ if (result) processedData = result;
561
+ }
562
+ const injectData = resolved.getInject(ctx, "update");
563
+ const data = {
564
+ ...processedData,
565
+ ...injectData
566
+ };
567
+ const [updated] = await ctx.db.update(table).set(data).where(where).returning();
568
+ if (h.afterUpdate) await h.afterUpdate(ctx, updated);
469
569
  return updated;
470
570
  }),
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();
571
+ delete: resolved.procedureFactory("delete").input(z.string()).mutation(async ({ ctx, input }) => {
572
+ await withGuard(ctx, "delete");
573
+ const where = buildWhere(ctx, "delete", eq(getIdColumn(), input));
574
+ const [existing] = await ctx.db.select().from(table).where(where);
575
+ await withAuthorize(ctx, existing, "delete");
576
+ if (!existing) throw new Error("Resource not found or access denied");
577
+ if (h.beforeDelete) await h.beforeDelete(ctx, input, existing);
578
+ let deleted;
579
+ if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
580
+ else [deleted] = await ctx.db.delete(table).where(where).returning();
581
+ if (h.afterDelete) await h.afterDelete(ctx, deleted);
475
582
  return deleted;
476
583
  }),
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 };
584
+ deleteMany: resolved.procedureFactory("deleteMany").input(z.array(z.string()).max(maxBatchSize)).mutation(async ({ ctx, input }) => {
585
+ await withGuard(ctx, "deleteMany");
586
+ if (h.beforeDeleteMany) await h.beforeDeleteMany(ctx, input);
587
+ const where = buildWhere(ctx, "deleteMany", inArray(getIdColumn(), input));
588
+ let result;
589
+ if (softDelete) result = { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
590
+ else result = { deleted: (await ctx.db.delete(table).where(where).returning()).length };
591
+ if (h.afterDeleteMany) await h.afterDeleteMany(ctx, result);
592
+ return result;
481
593
  }),
482
- updateMany: baseProcedure.input(z.object({
483
- ids: z.array(z.string()).max(maxBatchSize, `Maximum ${maxBatchSize} items allowed`),
594
+ updateMany: resolved.procedureFactory("updateMany").input(z.object({
595
+ ids: z.array(z.string()).max(maxBatchSize),
484
596
  data: updateSchema
485
597
  })).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 };
598
+ await withGuard(ctx, "updateMany");
599
+ let processedData = input.data;
600
+ if (h.beforeUpdateMany) {
601
+ const result$1 = await h.beforeUpdateMany(ctx, input.ids, processedData);
602
+ if (result$1) processedData = result$1;
603
+ }
604
+ const where = buildWhere(ctx, "updateMany", inArray(getIdColumn(), input.ids));
605
+ const injectData = resolved.getInject(ctx, "update");
606
+ const data = {
607
+ ...processedData,
608
+ ...injectData
609
+ };
610
+ const result = { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
611
+ if (h.afterUpdateMany) await h.afterUpdateMany(ctx, result);
612
+ return result;
489
613
  })
490
- });
614
+ };
615
+ const crudRouter = router(procedures);
616
+ return Object.assign(crudRouter, { procedures });
491
617
  }
492
618
 
493
619
  //#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.4.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/tsconfig": "0.1.0",
51
+ "@internal/prettier-config": "0.0.1",
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": {