@zeno-lib/db 0.3.0 → 0.3.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.
@@ -0,0 +1,93 @@
1
+ import { getTableName, is } from "drizzle-orm";
2
+ import { PgTable, getTableConfig } from "drizzle-orm/pg-core";
3
+ //#region src/triggers.ts
4
+ const DEFAULT_EXTENSION_SCHEMA = "extensions";
5
+ const DEFAULT_COLUMN = "updated_at";
6
+ const DEFAULT_AUTHOR_COLUMN = "updated_by";
7
+ const DEFAULT_TABLE_SCHEMA = "public";
8
+ const quote = (identifier) => `"${identifier.split("\"").join("\"\"")}"`;
9
+ function resolveTable(table, schema) {
10
+ const name = is(table, PgTable) ? getTableName(table) : table;
11
+ const tableSchema = schema ?? (is(table, PgTable) ? getTableConfig(table).schema ?? DEFAULT_TABLE_SCHEMA : DEFAULT_TABLE_SCHEMA);
12
+ return { qualified: `${quote(tableSchema)}.${quote(name)}` };
13
+ }
14
+ /**
15
+ * `CREATE TRIGGER` SQL that keeps `updated_at` current for **every** writer,
16
+ * including PostgREST, the dashboard and psql.
17
+ *
18
+ * `timestamps()` gives the column a `DEFAULT now()`, which covers the insert.
19
+ * SQL has no "on update" default, so without this trigger the column is only
20
+ * ever the insert time. Required, not optional.
21
+ *
22
+ * ```ts
23
+ * // pnpm db:generate --custom --name=posts_audit_triggers
24
+ * // then paste the output into the generated migration:
25
+ * console.log(updatedAtTrigger(posts))
26
+ * ```
27
+ */
28
+ function updatedAtTrigger(table, { column = DEFAULT_COLUMN, createExtension = true, extensionSchema = DEFAULT_EXTENSION_SCHEMA, name, schema } = {}) {
29
+ const { qualified } = resolveTable(table, schema);
30
+ const triggerName = name ?? `handle_${column}`;
31
+ const trigger = [
32
+ `drop trigger if exists ${quote(triggerName)} on ${qualified};`,
33
+ `create trigger ${quote(triggerName)}`,
34
+ ` before update on ${qualified}`,
35
+ " for each row",
36
+ ` execute function ${quote(extensionSchema)}.moddatetime (${quote(column)});`
37
+ ].join("\n");
38
+ return createExtension ? `${moddatetimeExtension(extensionSchema)}\n\n${trigger}` : trigger;
39
+ }
40
+ /** `create extension if not exists moddatetime`, safe to repeat per migration. */
41
+ function moddatetimeExtension(schema = DEFAULT_EXTENSION_SCHEMA) {
42
+ return `create extension if not exists moddatetime with schema ${quote(schema)};`;
43
+ }
44
+ /**
45
+ * `CREATE TRIGGER` SQL that stamps an author column with `auth.uid()` on every
46
+ * update, the `updated_at` story for `updated_by`.
47
+ *
48
+ * `updatedBy()` gives the column a `DEFAULT (select auth.uid())`, which covers
49
+ * the insert. This covers the update, for every writer rather than only the
50
+ * ones going through Drizzle.
51
+ *
52
+ * A write with no session (the admin or `service_role` clients) records `NULL`,
53
+ * because that is what `auth.uid()` returns there.
54
+ */
55
+ function updatedByTrigger(table, { column = DEFAULT_AUTHOR_COLUMN, createFunction = true, functionSchema = DEFAULT_TABLE_SCHEMA, name, schema } = {}) {
56
+ const { qualified } = resolveTable(table, schema);
57
+ const triggerName = name ?? `handle_${column}`;
58
+ const functionName = `set_${column}`;
59
+ const qualifiedFunction = `${quote(functionSchema)}.${quote(functionName)}`;
60
+ const routine = [
61
+ `create or replace function ${qualifiedFunction}()`,
62
+ " returns trigger",
63
+ " language plpgsql",
64
+ " security invoker",
65
+ ` set search_path = ''`,
66
+ "as $$",
67
+ "begin",
68
+ ` new.${quote(column)} = (select auth.uid());`,
69
+ " return new;",
70
+ "end;",
71
+ "$$;"
72
+ ].join("\n");
73
+ const trigger = [
74
+ `drop trigger if exists ${quote(triggerName)} on ${qualified};`,
75
+ `create trigger ${quote(triggerName)}`,
76
+ ` before update on ${qualified}`,
77
+ " for each row",
78
+ ` execute function ${qualifiedFunction}();`
79
+ ].join("\n");
80
+ return createFunction ? `${routine}\n\n${trigger}` : trigger;
81
+ }
82
+ /**
83
+ * Both update triggers `auditColumns()` needs, in one string.
84
+ *
85
+ * `auditColumns()` and `timestamps()` leave `updated_at` and `updated_by` at
86
+ * their insert values without these, so a table using either helper wants this
87
+ * in a `drizzle-kit generate --custom` migration.
88
+ */
89
+ function auditTriggers(table, options = {}) {
90
+ return `${updatedAtTrigger(table, options)}\n\n${updatedByTrigger(table, options)}`;
91
+ }
92
+ //#endregion
93
+ export { auditTriggers, moddatetimeExtension, updatedAtTrigger, updatedByTrigger };
package/package.json CHANGED
@@ -1,15 +1,36 @@
1
1
  {
2
2
  "name": "@zeno-lib/db",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "exports": {
6
- ".": "./src/index.ts",
7
- "./auth": "./src/auth-schema.ts",
8
- "./config": "./src/config.ts",
9
- "./schema": "./src/schema.ts",
10
- "./triggers": "./src/triggers.ts"
6
+ ".": {
7
+ "types": "./dist/index.d.mts",
8
+ "import": "./dist/index.mjs",
9
+ "default": "./dist/index.mjs"
10
+ },
11
+ "./auth": {
12
+ "types": "./dist/auth-schema.d.mts",
13
+ "import": "./dist/auth-schema.mjs",
14
+ "default": "./dist/auth-schema.mjs"
15
+ },
16
+ "./config": {
17
+ "types": "./dist/config.d.mts",
18
+ "import": "./dist/config.mjs",
19
+ "default": "./dist/config.mjs"
20
+ },
21
+ "./schema": {
22
+ "types": "./dist/schema.d.mts",
23
+ "import": "./dist/schema.mjs",
24
+ "default": "./dist/schema.mjs"
25
+ },
26
+ "./triggers": {
27
+ "types": "./dist/triggers.d.mts",
28
+ "import": "./dist/triggers.mjs",
29
+ "default": "./dist/triggers.mjs"
30
+ }
11
31
  },
12
32
  "files": [
33
+ "dist",
13
34
  "src"
14
35
  ],
15
36
  "publishConfig": {
@@ -33,12 +54,15 @@
33
54
  "drizzle-orm": "1.0.0-rc.3",
34
55
  "postgres": "3.4.7",
35
56
  "supabase": "2.84.1",
57
+ "tsdown": "^0.22.14",
58
+ "typescript": "7.0.2",
36
59
  "vite": "8.0.16",
37
60
  "vitest": "4.1.0",
38
61
  "@zeno-lib/test": "0.0.3",
39
62
  "@zeno-lib/typescript": "^1.1.0"
40
63
  },
41
64
  "scripts": {
65
+ "build": "tsdown",
42
66
  "dev": "pnpm exec supabase start",
43
67
  "stop": "pnpm exec supabase stop",
44
68
  "reset": "pnpm exec supabase db reset",