@zeno-lib/db 0.5.0 → 0.7.0-beta.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/config.mjs +40 -1
- package/dist/next.d.mts +11 -3
- package/dist/next.mjs +1 -1
- package/package.json +7 -8
- package/src/config.ts +8 -1
- package/src/define-action.ts +23 -7
- package/src/move-generated-sql.ts +80 -0
- package/src/next.test-d.ts +16 -0
- package/src/next.test.ts +17 -0
- package/src/next.ts +1 -0
package/dist/config.mjs
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
1
|
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
import { existsSync, readdirSync, renameSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { styleText } from "node:util";
|
|
5
|
+
//#region src/move-generated-sql.ts
|
|
6
|
+
const GENERATED_SQL = "migration.sql";
|
|
7
|
+
/**
|
|
8
|
+
* Move each `<name>/migration.sql` to `<name>.sql`,
|
|
9
|
+
* because the Supabase CLI only reads `.sql` files at the top of the folder.
|
|
10
|
+
* Throws if a `<name>.sql` already exists.
|
|
11
|
+
*/
|
|
12
|
+
function moveGeneratedSql(options) {
|
|
13
|
+
const { migrationsDir } = options;
|
|
14
|
+
const names = existsSync(migrationsDir) ? readdirSync(migrationsDir).filter((name) => existsSync(join(migrationsDir, name, GENERATED_SQL))).sort() : [];
|
|
15
|
+
const taken = names.map((name) => join(migrationsDir, `${name}.sql`)).filter((target) => existsSync(target));
|
|
16
|
+
if (taken.length > 0) throw new Error(`Cannot move the generated SQL, because these files already exist: ${taken.join(", ")}`);
|
|
17
|
+
return { moved: names.map((name) => {
|
|
18
|
+
const target = join(migrationsDir, `${name}.sql`);
|
|
19
|
+
renameSync(join(migrationsDir, name, GENERATED_SQL), target);
|
|
20
|
+
return target;
|
|
21
|
+
}) };
|
|
22
|
+
}
|
|
23
|
+
/** Run `moveGeneratedSql` when `drizzle-kit generate` exits. */
|
|
24
|
+
function moveGeneratedSqlAfterGenerate(options) {
|
|
25
|
+
if (process.argv[2] !== "generate") return;
|
|
26
|
+
process.once("exit", (code) => {
|
|
27
|
+
if (code !== 0) return;
|
|
28
|
+
try {
|
|
29
|
+
const check = styleText("green", "✓", { stream: process.stderr });
|
|
30
|
+
for (const path of moveGeneratedSql(options).moved) console.error(`${check} Moved the generated SQL to ${path}`);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
console.error(styleText("red", `✗ ${message}`, { stream: process.stderr }));
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
2
39
|
//#region src/config.ts
|
|
3
40
|
/**
|
|
4
41
|
* Roles a Supabase project ships that drizzle-kit's `provider: "supabase"` does
|
|
@@ -43,7 +80,7 @@ const DEFAULT_SCHEMA_FILTER = ["public"];
|
|
|
43
80
|
function defineDrizzleConfig(overrides = {}) {
|
|
44
81
|
const { entities, ...configOverrides } = overrides;
|
|
45
82
|
const roleOverrides = typeof entities?.roles === "object" ? entities.roles : {};
|
|
46
|
-
|
|
83
|
+
const config = defineConfig({
|
|
47
84
|
dbCredentials: { url: process.env.SUPABASE_DATABASE_URL ?? "" },
|
|
48
85
|
dialect: "postgresql",
|
|
49
86
|
entities: {
|
|
@@ -59,6 +96,8 @@ function defineDrizzleConfig(overrides = {}) {
|
|
|
59
96
|
schemaFilter: DEFAULT_SCHEMA_FILTER,
|
|
60
97
|
...configOverrides
|
|
61
98
|
});
|
|
99
|
+
if (config.out) moveGeneratedSqlAfterGenerate({ migrationsDir: config.out });
|
|
100
|
+
return config;
|
|
62
101
|
}
|
|
63
102
|
//#endregion
|
|
64
103
|
export { defineDrizzleConfig, supabaseManagedRoles };
|
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.
|
|
3
|
+
"version": "0.7.0-beta.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.
|
|
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": "
|
|
75
|
-
"drizzle-kit": "1.0.0-rc.
|
|
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": "
|
|
83
|
+
"vitest": "5.0.2",
|
|
84
84
|
"zod": "^4.4.3",
|
|
85
85
|
"@zeno-lib/schema": "0.2.2",
|
|
86
|
-
"@zeno-lib/
|
|
87
|
-
"@zeno-lib/
|
|
86
|
+
"@zeno-lib/test": "0.1.0",
|
|
87
|
+
"@zeno-lib/typescript": "^1.1.0"
|
|
88
88
|
},
|
|
89
89
|
"scripts": {
|
|
90
90
|
"build": "tsdown",
|
|
@@ -92,7 +92,6 @@
|
|
|
92
92
|
"stop": "pnpm exec supabase stop",
|
|
93
93
|
"reset": "pnpm exec supabase db reset",
|
|
94
94
|
"db:generate": "drizzle-kit generate",
|
|
95
|
-
"db:migrate": "drizzle-kit migrate",
|
|
96
95
|
"db:push": "drizzle-kit push",
|
|
97
96
|
"db:studio": "drizzle-kit studio",
|
|
98
97
|
"test": "vitest --run",
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// https://orm.drizzle.team/docs/drizzle-config-file
|
|
2
2
|
// https://orm.drizzle.team/docs/rls#migrations (entities.roles.provider: "supabase")
|
|
3
3
|
import { type Config, defineConfig } from "drizzle-kit"
|
|
4
|
+
import { moveGeneratedSqlAfterGenerate } from "./move-generated-sql.ts"
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Roles a Supabase project ships that drizzle-kit's `provider: "supabase"` does
|
|
@@ -54,7 +55,7 @@ export function defineDrizzleConfig(
|
|
|
54
55
|
const roleOverrides =
|
|
55
56
|
typeof entities?.roles === "object" ? entities.roles : {}
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
const config = defineConfig({
|
|
58
59
|
dbCredentials: { url: process.env.SUPABASE_DATABASE_URL ?? "" },
|
|
59
60
|
dialect: "postgresql",
|
|
60
61
|
// Tells drizzle-kit that Supabase's built-in roles (anon, authenticated,
|
|
@@ -74,4 +75,10 @@ export function defineDrizzleConfig(
|
|
|
74
75
|
schemaFilter: DEFAULT_SCHEMA_FILTER,
|
|
75
76
|
...configOverrides,
|
|
76
77
|
} as Config)
|
|
78
|
+
|
|
79
|
+
if (config.out) {
|
|
80
|
+
moveGeneratedSqlAfterGenerate({ migrationsDir: config.out })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return config
|
|
77
84
|
}
|
package/src/define-action.ts
CHANGED
|
@@ -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
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
84
|
+
return (result ?? null) as ActionValue<TResult>
|
|
85
|
+
}
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
/**
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, readdirSync, renameSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { styleText } from "node:util"
|
|
4
|
+
|
|
5
|
+
export type MoveGeneratedSqlOptions = {
|
|
6
|
+
/** drizzle-kit writes migrations to this folder. */
|
|
7
|
+
migrationsDir: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type MoveGeneratedSqlResult = {
|
|
11
|
+
/** The new paths of the SQL files, sorted by name. */
|
|
12
|
+
moved: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const GENERATED_SQL = "migration.sql"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Move each `<name>/migration.sql` to `<name>.sql`,
|
|
19
|
+
* because the Supabase CLI only reads `.sql` files at the top of the folder.
|
|
20
|
+
* Throws if a `<name>.sql` already exists.
|
|
21
|
+
*/
|
|
22
|
+
export function moveGeneratedSql(
|
|
23
|
+
options: MoveGeneratedSqlOptions
|
|
24
|
+
): MoveGeneratedSqlResult {
|
|
25
|
+
const { migrationsDir } = options
|
|
26
|
+
|
|
27
|
+
const names = existsSync(migrationsDir)
|
|
28
|
+
? readdirSync(migrationsDir)
|
|
29
|
+
.filter((name) => existsSync(join(migrationsDir, name, GENERATED_SQL)))
|
|
30
|
+
.sort()
|
|
31
|
+
: []
|
|
32
|
+
|
|
33
|
+
const taken = names
|
|
34
|
+
.map((name) => join(migrationsDir, `${name}.sql`))
|
|
35
|
+
.filter((target) => existsSync(target))
|
|
36
|
+
if (taken.length > 0) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Cannot move the generated SQL, because these files already exist: ${taken.join(", ")}`
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const moved = names.map((name) => {
|
|
43
|
+
const target = join(migrationsDir, `${name}.sql`)
|
|
44
|
+
renameSync(join(migrationsDir, name, GENERATED_SQL), target)
|
|
45
|
+
return target
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return { moved }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Run `moveGeneratedSql` when `drizzle-kit generate` exits. */
|
|
52
|
+
export function moveGeneratedSqlAfterGenerate(
|
|
53
|
+
options: MoveGeneratedSqlOptions
|
|
54
|
+
): void {
|
|
55
|
+
// drizzle-kit loads the config for every command.
|
|
56
|
+
// Only `generate` writes a migration.
|
|
57
|
+
if (process.argv[2] !== "generate") {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process.once("exit", (code) => {
|
|
62
|
+
if (code !== 0) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Print to stderr, so the JSON from `--output json` on stdout stays valid.
|
|
67
|
+
try {
|
|
68
|
+
const check = styleText("green", "✓", { stream: process.stderr })
|
|
69
|
+
for (const path of moveGeneratedSql(options).moved) {
|
|
70
|
+
console.error(`${check} Moved the generated SQL to ${path}`)
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
74
|
+
console.error(
|
|
75
|
+
styleText("red", `✗ ${message}`, { stream: process.stderr })
|
|
76
|
+
)
|
|
77
|
+
process.exitCode = 1
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}
|
package/src/next.test-d.ts
CHANGED
|
@@ -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))(
|