@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/README.md +94 -35
- package/dist/index.cjs +138 -52
- package/dist/index.d.cts +166 -129
- package/dist/index.d.ts +165 -127
- package/dist/index.js +138 -52
- 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,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,
|
|
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
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
421
|
-
list:
|
|
422
|
-
await
|
|
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
|
|
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
|
|
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:
|
|
452
|
-
await
|
|
453
|
-
const
|
|
454
|
-
const [item] = await ctx.db.select().from(table).where(
|
|
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:
|
|
458
|
-
await
|
|
459
|
-
const
|
|
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:
|
|
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
|
|
467
|
-
const
|
|
468
|
-
const [
|
|
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:
|
|
472
|
-
await
|
|
473
|
-
const
|
|
474
|
-
const [
|
|
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:
|
|
478
|
-
await
|
|
479
|
-
const
|
|
480
|
-
return { deleted: (await ctx.db.
|
|
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:
|
|
483
|
-
ids: z.array(z.string()).max(maxBatchSize
|
|
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
|
|
487
|
-
const
|
|
488
|
-
|
|
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.
|
|
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/
|
|
53
|
+
"@internal/vitest-config": "0.1.0"
|
|
54
54
|
},
|
|
55
55
|
"prettier": "@internal/prettier-config",
|
|
56
56
|
"scripts": {
|