@zeno-lib/db 0.0.1 → 0.2.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/package.json +21 -19
- package/src/auth-schema.test.ts +59 -0
- package/src/auth-schema.ts +94 -0
- package/src/casing.test.ts +512 -28
- package/src/config.ts +51 -2
- package/src/schema.test-d.ts +121 -0
- package/src/schema.ts +429 -43
- package/src/triggers.test.ts +131 -0
- package/src/triggers.ts +171 -0
package/src/triggers.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// SQL text for the triggers a Supabase schema wants but drizzle-kit cannot
|
|
2
|
+
// generate. drizzle-orm 1.0.0-rc.3 exports no trigger API and the drizzle-kit
|
|
3
|
+
// bundle contains no `CREATE TRIGGER`, so triggers are not part of the snapshot
|
|
4
|
+
// format and `drizzle-kit generate` will never emit one from your schema.
|
|
5
|
+
//
|
|
6
|
+
// What it does give you is `drizzle-kit generate --custom`, which writes an
|
|
7
|
+
// empty, *tracked* migration (its snapshot links into the chain via `prevIds`)
|
|
8
|
+
// for you to fill in. These helpers produce the SQL that goes in it, so the
|
|
9
|
+
// text is versioned here rather than retyped per table.
|
|
10
|
+
import { getTableName, is } from "drizzle-orm"
|
|
11
|
+
import { getTableConfig, PgTable } from "drizzle-orm/pg-core"
|
|
12
|
+
|
|
13
|
+
// Supabase installs extensions into `extensions`, not `public`.
|
|
14
|
+
const DEFAULT_EXTENSION_SCHEMA = "extensions"
|
|
15
|
+
const DEFAULT_COLUMN = "updated_at"
|
|
16
|
+
const DEFAULT_AUTHOR_COLUMN = "updated_by"
|
|
17
|
+
const DEFAULT_TABLE_SCHEMA = "public"
|
|
18
|
+
|
|
19
|
+
type UpdatedAtTriggerOptions = {
|
|
20
|
+
/** Column the trigger maintains. Default `"updated_at"`. */
|
|
21
|
+
column?: string
|
|
22
|
+
/** Schema holding the table. Default `"public"`, or the table's own. */
|
|
23
|
+
schema?: string
|
|
24
|
+
/** Trigger name. Default `handle_<column>`. */
|
|
25
|
+
name?: string
|
|
26
|
+
/** Schema `moddatetime` is installed into. Default `"extensions"`. */
|
|
27
|
+
extensionSchema?: string
|
|
28
|
+
/** Emit `create extension if not exists`. Default `true`. */
|
|
29
|
+
createExtension?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const quote = (identifier: string) => `"${identifier.split('"').join('""')}"`
|
|
33
|
+
|
|
34
|
+
function resolveTable(table: PgTable | string, schema?: string) {
|
|
35
|
+
const name = is(table, PgTable) ? getTableName(table) : table
|
|
36
|
+
const tableSchema =
|
|
37
|
+
schema ??
|
|
38
|
+
(is(table, PgTable)
|
|
39
|
+
? (getTableConfig(table).schema ?? DEFAULT_TABLE_SCHEMA)
|
|
40
|
+
: DEFAULT_TABLE_SCHEMA)
|
|
41
|
+
|
|
42
|
+
return { qualified: `${quote(tableSchema)}.${quote(name)}` }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `CREATE TRIGGER` SQL that keeps `updated_at` current for **every** writer,
|
|
47
|
+
* including PostgREST, the dashboard and psql.
|
|
48
|
+
*
|
|
49
|
+
* `timestamps()` gives the column a `DEFAULT now()`, which covers the insert.
|
|
50
|
+
* SQL has no "on update" default, so without this trigger the column is only
|
|
51
|
+
* ever the insert time. Required, not optional.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* // pnpm db:generate --custom --name=posts_audit_triggers
|
|
55
|
+
* // then paste the output into the generated migration:
|
|
56
|
+
* console.log(updatedAtTrigger(posts))
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function updatedAtTrigger(
|
|
60
|
+
table: PgTable | string,
|
|
61
|
+
{
|
|
62
|
+
column = DEFAULT_COLUMN,
|
|
63
|
+
createExtension = true,
|
|
64
|
+
extensionSchema = DEFAULT_EXTENSION_SCHEMA,
|
|
65
|
+
name,
|
|
66
|
+
schema,
|
|
67
|
+
}: UpdatedAtTriggerOptions = {}
|
|
68
|
+
) {
|
|
69
|
+
const { qualified } = resolveTable(table, schema)
|
|
70
|
+
const triggerName = name ?? `handle_${column}`
|
|
71
|
+
|
|
72
|
+
// `moddatetime` takes the column name as a trigger argument and sets it to
|
|
73
|
+
// now() on the NEW row, so it wins over whatever the UPDATE supplied.
|
|
74
|
+
const trigger = [
|
|
75
|
+
`drop trigger if exists ${quote(triggerName)} on ${qualified};`,
|
|
76
|
+
`create trigger ${quote(triggerName)}`,
|
|
77
|
+
` before update on ${qualified}`,
|
|
78
|
+
" for each row",
|
|
79
|
+
` execute function ${quote(extensionSchema)}.moddatetime (${quote(column)});`,
|
|
80
|
+
].join("\n")
|
|
81
|
+
|
|
82
|
+
return createExtension
|
|
83
|
+
? `${moddatetimeExtension(extensionSchema)}\n\n${trigger}`
|
|
84
|
+
: trigger
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** `create extension if not exists moddatetime`, safe to repeat per migration. */
|
|
88
|
+
export function moddatetimeExtension(schema = DEFAULT_EXTENSION_SCHEMA) {
|
|
89
|
+
return `create extension if not exists moddatetime with schema ${quote(schema)};`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type UpdatedByTriggerOptions = {
|
|
93
|
+
/** Column the trigger maintains. Default `"updated_by"`. */
|
|
94
|
+
column?: string
|
|
95
|
+
/** Schema holding the table. Default `"public"`, or the table's own. */
|
|
96
|
+
schema?: string
|
|
97
|
+
/** Trigger name. Default `handle_<column>`. */
|
|
98
|
+
name?: string
|
|
99
|
+
/** Schema the helper function is created in. Default `"public"`. */
|
|
100
|
+
functionSchema?: string
|
|
101
|
+
/** Emit `create or replace function`. Default `true`. */
|
|
102
|
+
createFunction?: boolean
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `CREATE TRIGGER` SQL that stamps an author column with `auth.uid()` on every
|
|
107
|
+
* update, the `updated_at` story for `updated_by`.
|
|
108
|
+
*
|
|
109
|
+
* `updatedBy()` gives the column a `DEFAULT (select auth.uid())`, which covers
|
|
110
|
+
* the insert. This covers the update, for every writer rather than only the
|
|
111
|
+
* ones going through Drizzle.
|
|
112
|
+
*
|
|
113
|
+
* A write with no session (the admin or `service_role` clients) records `NULL`,
|
|
114
|
+
* because that is what `auth.uid()` returns there.
|
|
115
|
+
*/
|
|
116
|
+
export function updatedByTrigger(
|
|
117
|
+
table: PgTable | string,
|
|
118
|
+
{
|
|
119
|
+
column = DEFAULT_AUTHOR_COLUMN,
|
|
120
|
+
createFunction = true,
|
|
121
|
+
functionSchema = DEFAULT_TABLE_SCHEMA,
|
|
122
|
+
name,
|
|
123
|
+
schema,
|
|
124
|
+
}: UpdatedByTriggerOptions = {}
|
|
125
|
+
) {
|
|
126
|
+
const { qualified } = resolveTable(table, schema)
|
|
127
|
+
const triggerName = name ?? `handle_${column}`
|
|
128
|
+
const functionName = `set_${column}`
|
|
129
|
+
const qualifiedFunction = `${quote(functionSchema)}.${quote(functionName)}`
|
|
130
|
+
|
|
131
|
+
// One function serves every table using this column name. `security invoker`
|
|
132
|
+
// is deliberate: auth.uid() reads a transaction-local setting, so the trigger
|
|
133
|
+
// needs no privileges of its own.
|
|
134
|
+
const routine = [
|
|
135
|
+
`create or replace function ${qualifiedFunction}()`,
|
|
136
|
+
" returns trigger",
|
|
137
|
+
" language plpgsql",
|
|
138
|
+
" security invoker",
|
|
139
|
+
` set search_path = ''`,
|
|
140
|
+
"as $$",
|
|
141
|
+
"begin",
|
|
142
|
+
` new.${quote(column)} = (select auth.uid());`,
|
|
143
|
+
" return new;",
|
|
144
|
+
"end;",
|
|
145
|
+
"$$;",
|
|
146
|
+
].join("\n")
|
|
147
|
+
|
|
148
|
+
const trigger = [
|
|
149
|
+
`drop trigger if exists ${quote(triggerName)} on ${qualified};`,
|
|
150
|
+
`create trigger ${quote(triggerName)}`,
|
|
151
|
+
` before update on ${qualified}`,
|
|
152
|
+
" for each row",
|
|
153
|
+
` execute function ${qualifiedFunction}();`,
|
|
154
|
+
].join("\n")
|
|
155
|
+
|
|
156
|
+
return createFunction ? `${routine}\n\n${trigger}` : trigger
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Both update triggers `auditColumns()` needs, in one string.
|
|
161
|
+
*
|
|
162
|
+
* `auditColumns()` and `timestamps()` leave `updated_at` and `updated_by` at
|
|
163
|
+
* their insert values without these, so a table using either helper wants this
|
|
164
|
+
* in a `drizzle-kit generate --custom` migration.
|
|
165
|
+
*/
|
|
166
|
+
export function auditTriggers(
|
|
167
|
+
table: PgTable | string,
|
|
168
|
+
options: UpdatedAtTriggerOptions & UpdatedByTriggerOptions = {}
|
|
169
|
+
) {
|
|
170
|
+
return `${updatedAtTrigger(table, options)}\n\n${updatedByTrigger(table, options)}`
|
|
171
|
+
}
|