@zeno-lib/db 0.5.0 → 0.6.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/next.d.mts CHANGED
@@ -87,12 +87,20 @@ interface ActionContext<TDb> {
87
87
  }
88
88
  /** An action's body: the RLS-bound `db`, the parsed input, and the context. */
89
89
  type ActionHandler<TContext extends ActionContext<unknown>, TOutput, TResult> = (db: TContext["db"], input: TOutput, context: TContext) => TResult | Promise<TResult>;
90
+ /**
91
+ * What an action resolves to: the handler's result, with `undefined` (and
92
+ * `void`) replaced by `null`.
93
+ */
94
+ type ActionValue<TResult> = undefined extends TResult ? Exclude<Exclude<TResult, undefined>, void> | null : TResult;
90
95
  /**
91
96
  * `defineAction(schema, handler)` returns the server action itself: an async
92
97
  * function that parses its argument, resolves the request context, then calls
93
- * `handler(db, input, context)`.
98
+ * `handler(db, input, context)`. A handler that resolves to `undefined`, such
99
+ * as a Drizzle `findFirst` that matched no row, makes the action resolve to
100
+ * `null`, because TanStack Query rejects a query whose function resolves to
101
+ * `undefined`.
94
102
  */
95
- type DefineAction<TContext extends ActionContext<unknown>> = <TInput, TOutput, TResult>(schema: ActionSchema<TInput, TOutput>, handler: ActionHandler<TContext, TOutput, TResult>) => (input: TInput) => Promise<TResult>;
103
+ type DefineAction<TContext extends ActionContext<unknown>> = <TInput, TOutput, TResult>(schema: ActionSchema<TInput, TOutput>, handler: ActionHandler<TContext, TOutput, TResult>) => (input: TInput) => Promise<ActionValue<TResult>>;
96
104
  /**
97
105
  * The schema `defineFormAction` needs: the Standard Schema `validate`, which
98
106
  * reports failures as issues with paths instead of throwing. Zod 4 schemas
@@ -179,4 +187,4 @@ type RequestDb<TRelations extends AnyRelations = EmptyRelations> = {
179
187
  */
180
188
  declare function createRequestDb<TRelations extends AnyRelations = EmptyRelations>(options: CreateRequestDbOptions<TRelations>): RequestDb<TRelations>;
181
189
  //#endregion
182
- export { type ActionContext, type ActionError, type ActionHandler, type ActionIssue, type ActionResult, type ActionSchema, ClaimsSource, CreateRequestDbOptions, type DefineAction, type DefineFormAction, FieldValidationError, type FormActionSchema, RequestContext, RequestDb, UnauthenticatedError, createRequestDb, toActionError, toFieldName };
190
+ export { type ActionContext, type ActionError, type ActionHandler, type ActionIssue, type ActionResult, type ActionSchema, type ActionValue, ClaimsSource, CreateRequestDbOptions, type DefineAction, type DefineFormAction, FieldValidationError, type FormActionSchema, RequestContext, RequestDb, UnauthenticatedError, createRequestDb, toActionError, toFieldName };
package/dist/next.mjs CHANGED
@@ -81,7 +81,7 @@ function createDefineAction(getContext) {
81
81
  return (schema, handler) => async (input) => {
82
82
  const parsed = schema.parse(input);
83
83
  const context = await getContext();
84
- return await handler(context.db, parsed, context);
84
+ return await handler(context.db, parsed, context) ?? null;
85
85
  };
86
86
  }
87
87
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeno-lib/db",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@supabase/supabase-js": ">=2",
60
- "drizzle-kit": "1.0.0-rc.3",
60
+ "drizzle-kit": "1.0.0-rc.5-5935859",
61
61
  "drizzle-orm": "1.0.0-rc.3",
62
62
  "postgres": ">=3.4",
63
63
  "react": ">=19"
@@ -71,8 +71,8 @@
71
71
  "@supabase/supabase-js": "2.116.0",
72
72
  "@types/node": "24.10.0",
73
73
  "@types/react": "19.2.14",
74
- "dotenv": "17.4.2",
75
- "drizzle-kit": "1.0.0-rc.3",
74
+ "dotenv": "18.0.4",
75
+ "drizzle-kit": "1.0.0-rc.5-5935859",
76
76
  "drizzle-orm": "1.0.0-rc.3",
77
77
  "postgres": "3.4.7",
78
78
  "react": "^19.2.5",
@@ -80,11 +80,11 @@
80
80
  "tsdown": "^0.22.14",
81
81
  "typescript": "7.0.2",
82
82
  "vite": "8.0.16",
83
- "vitest": "4.1.11",
83
+ "vitest": "5.0.2",
84
84
  "zod": "^4.4.3",
85
85
  "@zeno-lib/schema": "0.2.2",
86
- "@zeno-lib/typescript": "^1.1.0",
87
- "@zeno-lib/test": "0.1.0"
86
+ "@zeno-lib/test": "0.1.0",
87
+ "@zeno-lib/typescript": "^1.1.0"
88
88
  },
89
89
  "scripts": {
90
90
  "build": "tsdown",
@@ -36,10 +36,21 @@ export type ActionHandler<
36
36
  context: TContext
37
37
  ) => TResult | Promise<TResult>
38
38
 
39
+ /**
40
+ * What an action resolves to: the handler's result, with `undefined` (and
41
+ * `void`) replaced by `null`.
42
+ */
43
+ export type ActionValue<TResult> = undefined extends TResult
44
+ ? Exclude<Exclude<TResult, undefined>, void> | null
45
+ : TResult
46
+
39
47
  /**
40
48
  * `defineAction(schema, handler)` returns the server action itself: an async
41
49
  * function that parses its argument, resolves the request context, then calls
42
- * `handler(db, input, context)`.
50
+ * `handler(db, input, context)`. A handler that resolves to `undefined`, such
51
+ * as a Drizzle `findFirst` that matched no row, makes the action resolve to
52
+ * `null`, because TanStack Query rejects a query whose function resolves to
53
+ * `undefined`.
43
54
  */
44
55
  export type DefineAction<TContext extends ActionContext<unknown>> = <
45
56
  TInput,
@@ -48,7 +59,7 @@ export type DefineAction<TContext extends ActionContext<unknown>> = <
48
59
  >(
49
60
  schema: ActionSchema<TInput, TOutput>,
50
61
  handler: ActionHandler<TContext, TOutput, TResult>
51
- ) => (input: TInput) => Promise<TResult>
62
+ ) => (input: TInput) => Promise<ActionValue<TResult>>
52
63
 
53
64
  /**
54
65
  * Binds `defineAction` to a request-context resolver. Deliberately not a
@@ -61,12 +72,17 @@ export type DefineAction<TContext extends ActionContext<unknown>> = <
61
72
  export function createDefineAction<TContext extends ActionContext<unknown>>(
62
73
  getContext: () => Promise<TContext>
63
74
  ): DefineAction<TContext> {
64
- return (schema, handler) => async (input) => {
65
- const parsed = schema.parse(input)
66
- const context = await getContext()
75
+ return <TInput, TOutput, TResult>(
76
+ schema: ActionSchema<TInput, TOutput>,
77
+ handler: ActionHandler<TContext, TOutput, TResult>
78
+ ) =>
79
+ async (input: TInput) => {
80
+ const parsed = schema.parse(input)
81
+ const context = await getContext()
82
+ const result = await handler(context.db, parsed, context)
67
83
 
68
- return await handler(context.db, parsed, context)
69
- }
84
+ return (result ?? null) as ActionValue<TResult>
85
+ }
70
86
  }
71
87
 
72
88
  /**
@@ -31,6 +31,22 @@ test("a synchronous handler still yields an async action", () => {
31
31
  expectTypeOf(action).returns.toEqualTypeOf<Promise<boolean>>()
32
32
  })
33
33
 
34
+ test("a result that may be undefined resolves to null instead", () => {
35
+ const action = defineAction(z.number(), (_db, input) =>
36
+ Promise.resolve(input > 0 ? { id: input } : undefined)
37
+ )
38
+
39
+ expectTypeOf(action).returns.toEqualTypeOf<Promise<{ id: number } | null>>()
40
+ })
41
+
42
+ test("a handler with no result resolves to null", () => {
43
+ const action = defineAction(z.number(), async () => {
44
+ await Promise.resolve()
45
+ })
46
+
47
+ expectTypeOf(action).returns.toEqualTypeOf<Promise<null>>()
48
+ })
49
+
34
50
  test("defineFormAction takes the schema's input and resolves to an ActionResult", () => {
35
51
  const schema = z.object({ id: z.string().transform(Number) })
36
52
  const action = defineFormAction(schema, (db, input) => {
package/src/next.test.ts CHANGED
@@ -79,6 +79,23 @@ describe("defineAction", () => {
79
79
  expect(handler).toHaveBeenCalledWith(context.db, { id: 7 }, context)
80
80
  })
81
81
 
82
+ it("resolves to null when the handler resolves to undefined", async () => {
83
+ const action = createDefineAction(() => Promise.resolve({ db: {} }))(
84
+ schema,
85
+ () => Promise.resolve(undefined)
86
+ )
87
+
88
+ await expect(action({ id: 1 })).resolves.toBeNull()
89
+ })
90
+
91
+ it("keeps a falsy result other than undefined", async () => {
92
+ const define = createDefineAction(() => Promise.resolve({ db: {} }))
93
+
94
+ await expect(define(schema, () => 0)({ id: 1 })).resolves.toBe(0)
95
+ await expect(define(schema, () => false)({ id: 1 })).resolves.toBe(false)
96
+ await expect(define(schema, () => "")({ id: 1 })).resolves.toBe("")
97
+ })
98
+
82
99
  it("hands the handler the claims for authorship", async () => {
83
100
  const context = { claims: { sub: "user-1" }, db: {} }
84
101
  const action = createDefineAction(() => Promise.resolve(context))(
package/src/next.ts CHANGED
@@ -26,6 +26,7 @@ export type {
26
26
  ActionContext,
27
27
  ActionHandler,
28
28
  ActionSchema,
29
+ ActionValue,
29
30
  DefineAction,
30
31
  DefineFormAction,
31
32
  FormActionSchema,