@wordrhyme/auto-crud-server 0.6.1 → 0.6.2

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.cjs CHANGED
@@ -493,7 +493,7 @@ function createCrudRouter(config) {
493
493
  const conditions = [];
494
494
  const scopeCondition = resolved.getScope(ctx, table, operation);
495
495
  if (scopeCondition) conditions.push(scopeCondition);
496
- if (softDelete && (operation === "list" || operation === "get")) {
496
+ if (softDelete) {
497
497
  const col = getSoftDeleteColumn();
498
498
  if (col) conditions.push((0, drizzle_orm.isNull)(col));
499
499
  }
@@ -507,15 +507,24 @@ function createCrudRouter(config) {
507
507
  if (operation === "upsert") {
508
508
  const canCreate = await resolved.guard(ctx, "create");
509
509
  const canUpdate = await resolved.guard(ctx, "update");
510
- if (!canCreate || !canUpdate) throw new Error("Forbidden: upsert requires both create and update permissions");
510
+ if (!canCreate || !canUpdate) throw new __trpc_server.TRPCError({
511
+ code: "FORBIDDEN",
512
+ message: "Forbidden: upsert requires both create and update permissions"
513
+ });
511
514
  return;
512
515
  }
513
- if (!await resolved.guard(ctx, operation)) throw new Error(`Forbidden: ${operation} not allowed`);
516
+ if (!await resolved.guard(ctx, operation)) throw new __trpc_server.TRPCError({
517
+ code: "FORBIDDEN",
518
+ message: `Forbidden: ${operation} not allowed`
519
+ });
514
520
  }
515
521
  };
516
522
  const withAuthorize = async (ctx, resource, operation) => {
517
523
  if (!resource) return;
518
- if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new Error(`Forbidden: Cannot ${operation} this resource`);
524
+ if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new __trpc_server.TRPCError({
525
+ code: "FORBIDDEN",
526
+ message: `Forbidden: Cannot ${operation} this resource`
527
+ });
519
528
  };
520
529
  const procedures = {
521
530
  list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
@@ -598,7 +607,10 @@ function createCrudRouter(config) {
598
607
  const where = buildWhere(ctx, "update", (0, drizzle_orm.eq)(getIdColumn(), input.id));
599
608
  const [existing] = await ctx.db.select().from(table).where(where);
600
609
  await withAuthorize(ctx, existing, "update");
601
- if (!existing) throw new Error("Resource not found or access denied");
610
+ if (!existing) throw new __trpc_server.TRPCError({
611
+ code: "NOT_FOUND",
612
+ message: "Resource not found or access denied"
613
+ });
602
614
  const doUpdate = async (updateData) => {
603
615
  const injectData = resolved.getInject(ctx, "update");
604
616
  const data = {
@@ -622,7 +634,10 @@ function createCrudRouter(config) {
622
634
  const where = buildWhere(ctx, "delete", (0, drizzle_orm.eq)(getIdColumn(), input));
623
635
  const [existing] = await ctx.db.select().from(table).where(where);
624
636
  await withAuthorize(ctx, existing, "delete");
625
- if (!existing) throw new Error("Resource not found or access denied");
637
+ if (!existing) throw new __trpc_server.TRPCError({
638
+ code: "NOT_FOUND",
639
+ message: "Resource not found or access denied"
640
+ });
626
641
  const doDelete = async () => {
627
642
  let deleted;
628
643
  if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
@@ -641,8 +656,8 @@ function createCrudRouter(config) {
641
656
  await withGuard(ctx, "deleteMany");
642
657
  const where = buildWhere(ctx, "deleteMany", (0, drizzle_orm.inArray)(getIdColumn(), input));
643
658
  const doDeleteMany = async () => {
644
- if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
645
- else return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
659
+ if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning({ id: getIdColumn() })).length };
660
+ else return { deleted: (await ctx.db.delete(table).where(where).returning({ id: getIdColumn() })).length };
646
661
  };
647
662
  if (m.deleteMany) return m.deleteMany({
648
663
  ctx,
@@ -663,7 +678,7 @@ function createCrudRouter(config) {
663
678
  ...updateData,
664
679
  ...injectData
665
680
  };
666
- return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
681
+ return { updated: (await ctx.db.update(table).set(data).where(where).returning({ id: getIdColumn() })).length };
667
682
  };
668
683
  if (m.updateMany) return m.updateMany({
669
684
  ctx,
@@ -676,21 +691,27 @@ function createCrudRouter(config) {
676
691
  upsert: resolved.procedureFactory("upsert").input(insertSchema).mutation(async ({ ctx, input }) => {
677
692
  await withGuard(ctx, "upsert");
678
693
  const doUpsert = async (inputData) => {
679
- const injectData = resolved.getInject(ctx, "create");
680
- const data = {
681
- ...inputData,
682
- ...injectData
683
- };
684
694
  const inputId = inputData[idField];
685
695
  let isNew = true;
696
+ let existing = null;
686
697
  if (inputId) {
687
698
  const where = buildWhere(ctx, "get", (0, drizzle_orm.eq)(getIdColumn(), inputId));
688
- const [existing] = await ctx.db.select().from(table).where(where);
699
+ const [found] = await ctx.db.select().from(table).where(where);
700
+ existing = found;
689
701
  isNew = !existing;
702
+ if (!isNew && existing) await withAuthorize(ctx, existing, "update");
690
703
  }
704
+ const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
705
+ const data = {
706
+ ...inputData,
707
+ ...injectData
708
+ };
691
709
  const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
692
710
  target: getIdColumn(),
693
- set: data
711
+ set: {
712
+ ...inputData,
713
+ ...resolved.getInject(ctx, "update")
714
+ }
694
715
  }).returning();
695
716
  return {
696
717
  data: result,
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { and, asc, desc, eq, gt, gte, ilike, inArray, isNull, lt, lte, ne, not, notIlike, notInArray, or, sql } from "drizzle-orm";
3
- import { initTRPC } from "@trpc/server";
3
+ import { TRPCError, initTRPC } from "@trpc/server";
4
4
  import superjson from "superjson";
5
5
  import { addDays, endOfDay, startOfDay } from "date-fns";
6
6
 
@@ -465,7 +465,7 @@ function createCrudRouter(config) {
465
465
  const conditions = [];
466
466
  const scopeCondition = resolved.getScope(ctx, table, operation);
467
467
  if (scopeCondition) conditions.push(scopeCondition);
468
- if (softDelete && (operation === "list" || operation === "get")) {
468
+ if (softDelete) {
469
469
  const col = getSoftDeleteColumn();
470
470
  if (col) conditions.push(isNull(col));
471
471
  }
@@ -479,15 +479,24 @@ function createCrudRouter(config) {
479
479
  if (operation === "upsert") {
480
480
  const canCreate = await resolved.guard(ctx, "create");
481
481
  const canUpdate = await resolved.guard(ctx, "update");
482
- if (!canCreate || !canUpdate) throw new Error("Forbidden: upsert requires both create and update permissions");
482
+ if (!canCreate || !canUpdate) throw new TRPCError({
483
+ code: "FORBIDDEN",
484
+ message: "Forbidden: upsert requires both create and update permissions"
485
+ });
483
486
  return;
484
487
  }
485
- if (!await resolved.guard(ctx, operation)) throw new Error(`Forbidden: ${operation} not allowed`);
488
+ if (!await resolved.guard(ctx, operation)) throw new TRPCError({
489
+ code: "FORBIDDEN",
490
+ message: `Forbidden: ${operation} not allowed`
491
+ });
486
492
  }
487
493
  };
488
494
  const withAuthorize = async (ctx, resource, operation) => {
489
495
  if (!resource) return;
490
- if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new Error(`Forbidden: Cannot ${operation} this resource`);
496
+ if (!await resolved.checkAuthorize(ctx, resource, operation)) throw new TRPCError({
497
+ code: "FORBIDDEN",
498
+ message: `Forbidden: Cannot ${operation} this resource`
499
+ });
491
500
  };
492
501
  const procedures = {
493
502
  list: resolved.procedureFactory("list").input(listInputSchema).query(async ({ ctx, input }) => {
@@ -570,7 +579,10 @@ function createCrudRouter(config) {
570
579
  const where = buildWhere(ctx, "update", eq(getIdColumn(), input.id));
571
580
  const [existing] = await ctx.db.select().from(table).where(where);
572
581
  await withAuthorize(ctx, existing, "update");
573
- if (!existing) throw new Error("Resource not found or access denied");
582
+ if (!existing) throw new TRPCError({
583
+ code: "NOT_FOUND",
584
+ message: "Resource not found or access denied"
585
+ });
574
586
  const doUpdate = async (updateData) => {
575
587
  const injectData = resolved.getInject(ctx, "update");
576
588
  const data = {
@@ -594,7 +606,10 @@ function createCrudRouter(config) {
594
606
  const where = buildWhere(ctx, "delete", eq(getIdColumn(), input));
595
607
  const [existing] = await ctx.db.select().from(table).where(where);
596
608
  await withAuthorize(ctx, existing, "delete");
597
- if (!existing) throw new Error("Resource not found or access denied");
609
+ if (!existing) throw new TRPCError({
610
+ code: "NOT_FOUND",
611
+ message: "Resource not found or access denied"
612
+ });
598
613
  const doDelete = async () => {
599
614
  let deleted;
600
615
  if (softDelete) [deleted] = await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning();
@@ -613,8 +628,8 @@ function createCrudRouter(config) {
613
628
  await withGuard(ctx, "deleteMany");
614
629
  const where = buildWhere(ctx, "deleteMany", inArray(getIdColumn(), input));
615
630
  const doDeleteMany = async () => {
616
- if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning()).length };
617
- else return { deleted: (await ctx.db.delete(table).where(where).returning()).length };
631
+ if (softDelete) return { deleted: (await ctx.db.update(table).set({ [softDelete.column]: softDelete.getValue() }).where(where).returning({ id: getIdColumn() })).length };
632
+ else return { deleted: (await ctx.db.delete(table).where(where).returning({ id: getIdColumn() })).length };
618
633
  };
619
634
  if (m.deleteMany) return m.deleteMany({
620
635
  ctx,
@@ -635,7 +650,7 @@ function createCrudRouter(config) {
635
650
  ...updateData,
636
651
  ...injectData
637
652
  };
638
- return { updated: (await ctx.db.update(table).set(data).where(where).returning()).length };
653
+ return { updated: (await ctx.db.update(table).set(data).where(where).returning({ id: getIdColumn() })).length };
639
654
  };
640
655
  if (m.updateMany) return m.updateMany({
641
656
  ctx,
@@ -648,21 +663,27 @@ function createCrudRouter(config) {
648
663
  upsert: resolved.procedureFactory("upsert").input(insertSchema).mutation(async ({ ctx, input }) => {
649
664
  await withGuard(ctx, "upsert");
650
665
  const doUpsert = async (inputData) => {
651
- const injectData = resolved.getInject(ctx, "create");
652
- const data = {
653
- ...inputData,
654
- ...injectData
655
- };
656
666
  const inputId = inputData[idField];
657
667
  let isNew = true;
668
+ let existing = null;
658
669
  if (inputId) {
659
670
  const where = buildWhere(ctx, "get", eq(getIdColumn(), inputId));
660
- const [existing] = await ctx.db.select().from(table).where(where);
671
+ const [found] = await ctx.db.select().from(table).where(where);
672
+ existing = found;
661
673
  isNew = !existing;
674
+ if (!isNew && existing) await withAuthorize(ctx, existing, "update");
662
675
  }
676
+ const injectData = resolved.getInject(ctx, isNew ? "create" : "update");
677
+ const data = {
678
+ ...inputData,
679
+ ...injectData
680
+ };
663
681
  const [result] = await ctx.db.insert(table).values(data).onConflictDoUpdate({
664
682
  target: getIdColumn(),
665
- set: data
683
+ set: {
684
+ ...inputData,
685
+ ...resolved.getInject(ctx, "update")
686
+ }
666
687
  }).returning();
667
688
  return {
668
689
  data: result,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wordrhyme/auto-crud-server",
3
3
  "type": "module",
4
- "version": "0.6.1",
4
+ "version": "0.6.2",
5
5
  "description": "tRPC server utilities for auto-crud - automatic CRUD routers for Drizzle ORM",
6
6
  "author": "wordrhyme",
7
7
  "license": "MIT",
@@ -45,12 +45,13 @@
45
45
  "superjson": "^2.2.6",
46
46
  "tsdown": "^0.15.12",
47
47
  "typescript": "^5.9.3",
48
+ "vitest": "^4.0.18",
48
49
  "zod": "^4.1.13",
49
- "@internal/eslint-config": "0.3.0",
50
50
  "@internal/prettier-config": "0.0.1",
51
51
  "@internal/tsconfig": "0.1.0",
52
+ "@internal/tsdown-config": "0.1.0",
52
53
  "@internal/vitest-config": "0.1.0",
53
- "@internal/tsdown-config": "0.1.0"
54
+ "@internal/eslint-config": "0.3.0"
54
55
  },
55
56
  "prettier": "@internal/prettier-config",
56
57
  "scripts": {
@@ -60,6 +61,7 @@
60
61
  "build:watch": "tsdown --watch",
61
62
  "test": "vitest --run --passWithNoTests",
62
63
  "test:watch": "vitest --watch",
64
+ "test:coverage": "vitest --run --coverage",
63
65
  "typecheck": "tsc --noEmit",
64
66
  "lint": "eslint",
65
67
  "format": "prettier --check . --ignore-path ../../.gitignore"