@zeno-lib/db 0.3.2 → 0.5.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/src/schema.ts CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  type PgBigInt64Builder,
11
11
  type PgIntegerBuilder,
12
12
  type PgPolicyConfig,
13
+ type PgTimestampBuilder,
14
+ type PgTimestampStringBuilder,
13
15
  type PgUUIDBuilder,
14
16
  type Precision,
15
17
  pgPolicy,
@@ -24,6 +26,12 @@ import {
24
26
  import { snakeCase } from "drizzle-orm/pg-core/casing"
25
27
  import { authenticatedRole, authUid } from "drizzle-orm/supabase"
26
28
  import { authUsers } from "./auth-schema.ts"
29
+ import {
30
+ DEFAULT_FUNCTION_PREFIX,
31
+ FUNCTION_POLICY_OPERATIONS,
32
+ type FunctionPolicyOperation,
33
+ functionPolicyName,
34
+ } from "./function-names.ts"
27
35
 
28
36
  // pg-core primitives without the `pg` prefix they repeat at every call site.
29
37
  // `table` is missing on purpose. Zeno's own is at the bottom of this file.
@@ -56,12 +64,34 @@ export {
56
64
  supabaseAuthAdminRole,
57
65
  } from "drizzle-orm/supabase"
58
66
 
59
- type TimestampsOptions = {
67
+ /** How a timestamp column is read back: a `Date` (the default) or Postgres's own text. */
68
+ export type TimestampMode = "date" | "string"
69
+
70
+ export type TimestampsOptions<TMode extends TimestampMode = "date"> = {
71
+ /**
72
+ * `"date"` (the default) reads a `Date`; `"string"` keeps Postgres's text
73
+ * form, which also keeps the microseconds a `Date` truncates. A JavaScript
74
+ * mapping only: it emits no SQL.
75
+ */
76
+ mode?: TMode
60
77
  withTimezone?: boolean
61
78
  /** Fractional-second digits. Postgres allows 0 to 6. */
62
79
  precision?: Precision
63
80
  }
64
81
 
82
+ // Mirrors drizzle's own `timestamp()` overload: only an exact `"string"` gives
83
+ // the string builder.
84
+ type TimestampColumn<TMode extends TimestampMode> = SetHasDefault<
85
+ SetNotNull<
86
+ [TMode] extends ["string"] ? PgTimestampStringBuilder : PgTimestampBuilder
87
+ >
88
+ >
89
+
90
+ export type TimestampColumns<TMode extends TimestampMode = "date"> = {
91
+ createdAt: TimestampColumn<TMode>
92
+ updatedAt: TimestampColumn<TMode>
93
+ }
94
+
65
95
  // `created_at` is a column DEFAULT, so Postgres fills it for every writer.
66
96
  // `updated_at` has no equivalent: SQL has no "on update" default, and Drizzle's
67
97
  // `$onUpdateFn` is applied while Drizzle builds its own statement, so a write
@@ -74,16 +104,22 @@ type TimestampsOptions = {
74
104
  // than a shared object, because Drizzle's builder methods mutate `this` and
75
105
  // return it. One builder in two tables would leak `.notNull()`, `.references()`
76
106
  // and its name from whichever table customised it first.
77
- export const timestamps = ({
107
+ export const timestamps = <TMode extends TimestampMode = "date">({
108
+ mode,
78
109
  precision,
79
110
  withTimezone = true,
80
- }: TimestampsOptions = {}) => {
81
- const config = { precision, withTimezone }
111
+ }: TimestampsOptions<TMode> = {}): TimestampColumns<TMode> => {
112
+ const column = (name: string) =>
113
+ mode === "string"
114
+ ? timestamp(name, { mode: "string", precision, withTimezone })
115
+ .notNull()
116
+ .defaultNow()
117
+ : timestamp(name, { precision, withTimezone }).notNull().defaultNow()
82
118
 
83
119
  return {
84
- createdAt: timestamp("created_at", config).notNull().defaultNow(),
85
- updatedAt: timestamp("updated_at", config).notNull().defaultNow(),
86
- }
120
+ createdAt: column("created_at"),
121
+ updatedAt: column("updated_at"),
122
+ } as TimestampColumns<TMode>
87
123
  }
88
124
 
89
125
  type ReferenceActions = ReferenceConfig["config"]
@@ -208,10 +244,13 @@ export const authorship = <TNotNull extends boolean = false>(
208
244
  })
209
245
 
210
246
  // Takes both halves' options, since it builds both halves.
211
- export const auditColumns = <TNotNull extends boolean = false>(
212
- options: AuthorshipOptions<TNotNull> & TimestampsOptions = {}
247
+ export const auditColumns = <
248
+ TNotNull extends boolean = false,
249
+ TMode extends TimestampMode = "date",
250
+ >(
251
+ options: AuthorshipOptions<TNotNull> & TimestampsOptions<TMode> = {}
213
252
  ) => ({
214
- ...timestamps(options),
253
+ ...timestamps<TMode>(options),
215
254
  ...authorship<TNotNull>(options),
216
255
  })
217
256
 
@@ -414,15 +453,6 @@ const POLICY_BUILDERS = {
414
453
  update: updatePolicy,
415
454
  } as const satisfies Record<PolicyOperation, typeof selectPolicy>
416
455
 
417
- const FUNCTION_POLICY_OPERATIONS = [
418
- "select",
419
- "insert",
420
- "update",
421
- "delete",
422
- ] as const
423
-
424
- type FunctionPolicyOperation = (typeof FUNCTION_POLICY_OPERATIONS)[number]
425
-
426
456
  /** Columns one function is called with. `null` calls it with none. */
427
457
  type FunctionArgument = AnyPgColumn | readonly AnyPgColumn[] | null
428
458
 
@@ -515,7 +545,7 @@ export const functionPolicies = (
515
545
  {
516
546
  argument,
517
547
  name,
518
- prefix = "can",
548
+ prefix = DEFAULT_FUNCTION_PREFIX,
519
549
  schema: functionSchema,
520
550
  }: FunctionPoliciesOptions = {}
521
551
  ) => {
@@ -528,7 +558,7 @@ export const functionPolicies = (
528
558
  const tableName = getTableName(getColumnTable(firstColumn))
529
559
 
530
560
  return FUNCTION_POLICY_OPERATIONS.map((operation) => {
531
- const functionName = `${prefix}_${operation}_${tableName}`
561
+ const functionName = functionPolicyName(prefix, operation, tableName)
532
562
  // Unqualified by default, so the name resolves through `search_path` the
533
563
  // way a hand-written policy would; `schema` pins it to the functions that
534
564
  // live beside their table instead.