@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/README.md +94 -35
- package/dist/index.cjs +188 -62
- package/dist/index.d.cts +293 -127
- package/dist/index.d.ts +283 -116
- package/dist/index.js +188 -62
- package/package.json +4 -4
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
|
-
|
|
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,
|
|
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
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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:
|
|
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 (
|
|
433
|
-
const sortField =
|
|
434
|
-
if (sortField && validateColumn(sortField.id, sortableColumns
|
|
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(
|
|
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
|
-
|
|
506
|
+
let listResult = {
|
|
444
507
|
data,
|
|
445
508
|
total: count,
|
|
446
|
-
page:
|
|
447
|
-
perPage:
|
|
448
|
-
pageCount: Math.ceil(count /
|
|
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:
|
|
452
|
-
await
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
|
|
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:
|
|
458
|
-
await
|
|
459
|
-
|
|
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:
|
|
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
|
|
467
|
-
const
|
|
468
|
-
const [
|
|
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:
|
|
472
|
-
await
|
|
473
|
-
const
|
|
474
|
-
const [
|
|
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:
|
|
478
|
-
await
|
|
479
|
-
|
|
480
|
-
|
|
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:
|
|
483
|
-
ids: z.array(z.string()).max(maxBatchSize
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
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.
|
|
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/
|
|
53
|
+
"@internal/vitest-config": "0.1.0"
|
|
54
54
|
},
|
|
55
55
|
"prettier": "@internal/prettier-config",
|
|
56
56
|
"scripts": {
|