okengine 0.17.2 → 0.18.3

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.
Files changed (197) hide show
  1. package/README.md +9 -5
  2. package/manifest.v1.schema.json +61 -2
  3. package/package.json +18 -4
  4. package/site/content/docs/elements/clock.mdx +1 -1
  5. package/site/content/docs/elements/flow.mdx +25 -1
  6. package/site/content/docs/elements/store.mdx +287 -341
  7. package/site/content/docs/elements/vault.mdx +5 -5
  8. package/site/content/docs/get-started/installation.mdx +1 -2
  9. package/site/content/docs/get-started/introduction.mdx +54 -110
  10. package/site/content/docs/get-started/meta.json +9 -1
  11. package/site/content/docs/get-started/testing.mdx +328 -0
  12. package/site/content/docs/get-started/why.mdx +94 -70
  13. package/site/content/docs/index.mdx +1 -1
  14. package/site/content/docs/plugins/apple.mdx +151 -0
  15. package/site/content/docs/plugins/discord.mdx +139 -0
  16. package/site/content/docs/plugins/facebook.mdx +134 -0
  17. package/site/content/docs/plugins/figma.mdx +138 -0
  18. package/site/content/docs/plugins/github.mdx +138 -0
  19. package/site/content/docs/plugins/google.mdx +153 -0
  20. package/site/content/docs/plugins/index.mdx +47 -1
  21. package/site/content/docs/plugins/meta.json +10 -0
  22. package/site/content/docs/plugins/microsoft.mdx +151 -0
  23. package/site/content/docs/plugins/oauth.mdx +188 -0
  24. package/site/content/docs/plugins/x.mdx +125 -0
  25. package/site/content/docs/reference/cli.md +3 -2
  26. package/site/content/docs/reference/client.mdx +58 -1
  27. package/site/content/docs/reference/configuration.mdx +2 -4
  28. package/site/content/docs/reference/fx.mdx +3 -1
  29. package/site/content/docs/reference/index.mdx +0 -5
  30. package/site/content/docs/reference/meta.json +2 -2
  31. package/site/content/docs/reference/okid.mdx +137 -0
  32. package/src/auth/bindings.ts +1 -1
  33. package/src/auth/config.ts +9 -0
  34. package/src/auth/identity-sql.ts +314 -0
  35. package/src/auth/identity.ts +140 -2
  36. package/src/auth/index.ts +17 -1
  37. package/src/auth/method-context.ts +3 -0
  38. package/src/auth/oauth-as/cimd.ts +132 -0
  39. package/src/auth/oauth-as/crypto.test.ts +101 -0
  40. package/src/auth/oauth-as/crypto.ts +393 -0
  41. package/src/auth/oauth-as/errors.ts +68 -0
  42. package/src/auth/oauth-as/http.test.ts +419 -0
  43. package/src/auth/oauth-as/http.ts +842 -0
  44. package/src/auth/oauth-as/stores.ts +61 -0
  45. package/src/auth/oauth-as/tables.ts +142 -0
  46. package/src/auth/tables.ts +0 -11
  47. package/src/bench/README.md +83 -0
  48. package/src/bench/REPORT.md +176 -0
  49. package/src/bench/g01-rls-stamp.bench.ts +194 -0
  50. package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
  51. package/src/bench/g03-signal-once.bench.ts +157 -0
  52. package/src/bench/g03-signal-reconnect.bench.ts +254 -0
  53. package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
  54. package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
  55. package/src/bench/g05-sustained-full.bench.ts +265 -0
  56. package/src/bench/g06-mixed-load.bench.ts +260 -0
  57. package/src/bench/g07-vault-crypto.bench.ts +100 -0
  58. package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
  59. package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
  60. package/src/bench/g08-store-kv-durable.bench.ts +133 -0
  61. package/src/bench/g08-store-sql.bench.ts +178 -0
  62. package/src/bench/g09-journal-sustained.bench.ts +203 -0
  63. package/src/bench/g10-observability-contention.bench.ts +246 -0
  64. package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
  65. package/src/bench/g13-elements.bench.ts +427 -0
  66. package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
  67. package/src/bench/g15-postgres-degradation.bench.ts +264 -0
  68. package/src/bench/g16-live-query-fanout.bench.ts +206 -0
  69. package/src/bench/lib/event-loop-lag.ts +26 -0
  70. package/src/bench/lib/infra.ts +60 -0
  71. package/src/bench/lib/report.ts +52 -0
  72. package/src/bench/lib/rss-sampler.ts +61 -0
  73. package/src/bench/lib/signal-pg.ts +88 -0
  74. package/src/bench/load-app.ts +337 -0
  75. package/src/bench/load-child.ts +108 -0
  76. package/src/bench/smoke.bench.ts +43 -0
  77. package/src/cli/competitor-mention-removal.test.ts +28 -0
  78. package/src/cli/doctor-fd.ts +117 -0
  79. package/src/cli/doctor.test.ts +192 -0
  80. package/src/cli/doctor.ts +129 -1
  81. package/src/client/create.ts +95 -1
  82. package/src/client/index.ts +9 -2
  83. package/src/client/transport.ts +11 -4
  84. package/src/client/use-live-query.ts +154 -0
  85. package/src/client-react/index.ts +15 -1
  86. package/src/client-react/live-resource.ts +246 -0
  87. package/src/client-react/use-live-query.test.ts +475 -0
  88. package/src/client-react/use-live-query.ts +530 -0
  89. package/src/compiler/extract.test.ts +518 -0
  90. package/src/compiler/extract.ts +386 -19
  91. package/src/console/server/invoke-user-flow.ts +2 -1
  92. package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
  94. package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
  95. package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
  96. package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
  97. package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
  98. package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
  99. package/src/console/ui-next/dist/index.html +1 -1
  100. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
  101. package/src/drivers/cdc-outbox.ts +389 -0
  102. package/src/drivers/memory.ts +20 -0
  103. package/src/drivers/oauth-apple.ts +156 -0
  104. package/src/drivers/oauth-discord.ts +79 -0
  105. package/src/drivers/oauth-facebook.ts +80 -0
  106. package/src/drivers/oauth-figma.ts +116 -0
  107. package/src/drivers/oauth-github.ts +92 -0
  108. package/src/drivers/oauth-google.ts +142 -0
  109. package/src/drivers/oauth-microsoft.ts +174 -0
  110. package/src/drivers/oauth-oidc.ts +293 -0
  111. package/src/drivers/oauth-shared.ts +326 -0
  112. package/src/drivers/oauth-types.ts +159 -0
  113. package/src/drivers/oauth-x.ts +77 -0
  114. package/src/drivers/oauth2-common.ts +95 -0
  115. package/src/drivers/oauth2-token.ts +61 -0
  116. package/src/drivers/pg-rls-row-passes.ts +251 -0
  117. package/src/drivers/pg-rls.ts +2 -0
  118. package/src/drivers/postgres.ts +45 -2
  119. package/src/drivers/signal-postgres.ts +2 -1
  120. package/src/elements/channel/runtime.ts +29 -2
  121. package/src/elements/channel.test.ts +52 -0
  122. package/src/elements/gate/boot.ts +29 -2
  123. package/src/elements/store/emit-drizzle.ts +147 -14
  124. package/src/elements/store/field-ddl.test.ts +118 -0
  125. package/src/elements/store/field-types.test.ts +455 -0
  126. package/src/elements/store/list-query.golden.json +777 -0
  127. package/src/elements/store/list-query.parity.test.ts +396 -0
  128. package/src/elements/store/list-query.ts +792 -0
  129. package/src/elements/store/live-default.test.ts +136 -0
  130. package/src/elements/store/live-http.test.ts +160 -0
  131. package/src/elements/store/live-isolation.test.ts +291 -0
  132. package/src/elements/store/live-query-runtime.test.ts +323 -0
  133. package/src/elements/store/live-query-runtime.ts +403 -0
  134. package/src/elements/store/live-query-server.test.ts +377 -0
  135. package/src/elements/store/live-query-server.ts +102 -0
  136. package/src/elements/store/live-query.ts +97 -0
  137. package/src/elements/store/resource.ts +189 -680
  138. package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
  139. package/src/elements/store/schema-decl.ts +539 -41
  140. package/src/elements/store/sql-rls-stamp.test.ts +27 -0
  141. package/src/elements/store/sql-session.ts +297 -35
  142. package/src/elements/store/table.ts +102 -21
  143. package/src/elements/store.test.ts +3 -1
  144. package/src/elements/store.ts +12 -1
  145. package/src/elements/vault/chaos-child.ts +74 -1
  146. package/src/elements/vault/chaos.test.ts +4 -2
  147. package/src/elements/vault/storage.ts +4 -2
  148. package/src/index.ts +4 -1
  149. package/src/kernel/app-auth.ts +1 -0
  150. package/src/kernel/app.ts +116 -2
  151. package/src/kernel/auth-sharing.test.ts +196 -0
  152. package/src/kernel/boot.test.ts +3 -3
  153. package/src/kernel/errors.ts +8 -0
  154. package/src/kernel/fx.test.ts +1 -0
  155. package/src/kernel/fx.ts +14 -2
  156. package/src/kernel/horizontal-child.ts +2 -1
  157. package/src/kernel/http-resource.ts +33 -7
  158. package/src/kernel/identity-host-persist.test.ts +119 -0
  159. package/src/kernel/instance-id.ts +4 -2
  160. package/src/kernel/journal.ts +2 -1
  161. package/src/kernel/mcp-tool.test.ts +95 -0
  162. package/src/kernel/on.ts +9 -0
  163. package/src/kernel/realtime-bind.ts +326 -0
  164. package/src/kernel/resource-live.ts +117 -0
  165. package/src/kernel/triggers.ts +86 -4
  166. package/src/manifest/diff.ts +37 -0
  167. package/src/manifest/types.ts +64 -2
  168. package/src/okid.bench.test.ts +64 -0
  169. package/src/okid.test.ts +338 -0
  170. package/src/okid.ts +245 -0
  171. package/src/plugins/anonymous.ts +19 -1
  172. package/src/plugins/auth/shared.ts +15 -0
  173. package/src/plugins/index.ts +2 -0
  174. package/src/plugins/magic-link.ts +10 -8
  175. package/src/plugins/mcp-oauth.ts +208 -0
  176. package/src/plugins/oauth/flow-store.ts +117 -0
  177. package/src/plugins/oauth/link.ts +69 -0
  178. package/src/plugins/oauth/shared.ts +108 -0
  179. package/src/plugins/oauth/token-vault.ts +100 -0
  180. package/src/plugins/oauth.security.test.ts +535 -0
  181. package/src/plugins/oauth.ts +532 -0
  182. package/src/plugins/otp.ts +48 -6
  183. package/src/plugins/passkey.ts +20 -1
  184. package/src/plugins/two-factor.ts +11 -0
  185. package/src/plugins/username.ts +40 -7
  186. package/src/release/build-lib.ts +7 -1
  187. package/src/release/measure.ts +1 -0
  188. package/src/release/official-plugins.ts +4 -1
  189. package/src/runs/collect.ts +2 -1
  190. package/src/runs/drivers/files.ts +2 -1
  191. package/src/test/create-test-app.ts +114 -5
  192. package/src/test/export-bundle.test.ts +33 -0
  193. package/src/test/live-signals.test.ts +83 -0
  194. package/src/test/tenant-isolation.test.ts +175 -0
  195. package/src/testing.ts +26 -0
  196. package/src/upgrade/codemods.ts +1 -1
  197. package/site/content/docs/reference/migrating-environments.mdx +0 -158
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Widened `field.*` surface — inference, options round-trip, emitter output,
3
+ * and runtime auto-DDL across the full Drizzle Postgres column types.
4
+ */
5
+
6
+ import { describe, expect, test } from "bun:test";
7
+ import type { FieldBuilder } from "./schema-decl.ts";
8
+ import { ddlTypeOf, resolveColumns, type ResolvedColumn } from "./table.ts";
9
+ import { emitColumnSource, emitDrizzleSource } from "./emit-drizzle.ts";
10
+ import { field, id, store } from "../store.ts";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Declare-site inference
14
+ // ---------------------------------------------------------------------------
15
+
16
+ describe("field.* — widened inference", () => {
17
+ test("text with enum narrows to literal union", () => {
18
+ const col = field
19
+ .text({ enum: ["todo", "doing", "done"] as const })
20
+ .notNull()
21
+ .finalize("status");
22
+ expect(col.sqlType).toBe("text");
23
+ });
24
+
25
+ test("varchar / char carry length + enum options", () => {
26
+ const v = field.varchar({ length: 256 }).finalize("slug");
27
+ expect(v.sqlType).toBe("varchar");
28
+ expect(v.typeOptions?.length).toBe(256);
29
+
30
+ const c = field.char({ length: 2 }).finalize("country");
31
+ expect(c.sqlType).toBe("char");
32
+ expect(c.typeOptions?.length).toBe(2);
33
+
34
+ const e = field.varchar({ enum: ["a", "b"] as const }).finalize("letter");
35
+ expect(e.sqlType).toBe("varchar");
36
+ expect(e.typeOptions?.enumValues).toEqual(["a", "b"]);
37
+ });
38
+
39
+ test("bigint modes round-trip through finalize", () => {
40
+ expect(field.bigint().finalize("a").typeOptions?.mode).toBe("number");
41
+ expect(field.bigint({ mode: "bigint" }).finalize("b").typeOptions?.mode).toBe("bigint");
42
+ expect(field.bigint({ mode: "string" }).finalize("c").typeOptions?.mode).toBe("string");
43
+ });
44
+
45
+ test("serial family implies notNull", () => {
46
+ expect(field.serial().finalize("s").notNull).toBe(true);
47
+ expect(field.smallserial().finalize("ss").notNull).toBe(true);
48
+ expect(field.bigserial().finalize("bs").notNull).toBe(true);
49
+ expect(field.bigserial({ mode: "bigint" }).finalize("bs2").typeOptions?.mode).toBe("bigint");
50
+ });
51
+
52
+ test("numeric precision/scale/mode round-trip; decimal aliases numeric", () => {
53
+ const n = field.numeric({ precision: 10, scale: 2 }).finalize("amount");
54
+ expect(n.sqlType).toBe("numeric");
55
+ expect(n.typeOptions?.precision).toBe(10);
56
+ expect(n.typeOptions?.scale).toBe(2);
57
+
58
+ const m = field.numeric({ mode: "number" }).finalize("amt_num");
59
+ expect(m.typeOptions?.mode).toBe("number");
60
+
61
+ const d = field.decimal({ precision: 6, scale: 3 }).finalize("rate");
62
+ expect(d.sqlType).toBe("numeric");
63
+ expect(d.typeOptions).toEqual({ precision: 6, scale: 3 });
64
+ });
65
+
66
+ test("temporal defaults are date mode; string opt-in recorded", () => {
67
+ // Default timestamp pins nothing at declare time — OKE's emitter pins mode.
68
+ const ts = field.timestamp().finalize("created");
69
+ expect(ts.sqlType).toBe("timestamp");
70
+ expect(ts.typeOptions?.mode).toBe("date");
71
+
72
+ const tsStr = field.timestamp({ mode: "string" }).finalize("wire_at");
73
+ expect(tsStr.typeOptions?.mode).toBe("string");
74
+
75
+ const tsTz = field.timestamp({ withTimezone: true, precision: 6 }).finalize("seen_at");
76
+ expect(tsTz.typeOptions?.withTimezone).toBe(true);
77
+ expect(tsTz.typeOptions?.precision).toBe(6);
78
+
79
+ const d = field.date().finalize("day");
80
+ expect(d.sqlType).toBe("date");
81
+ expect(d.typeOptions?.mode).toBe("date");
82
+
83
+ const dStr = field.date({ mode: "string" }).finalize("wire_day");
84
+ expect(dStr.typeOptions?.mode).toBe("string");
85
+ });
86
+
87
+ test("time is string-physics; interval carries fields qualifier", () => {
88
+ const t = field.time({ precision: 3 }).finalize("at");
89
+ expect(t.typeOptions?.precision).toBe(3);
90
+
91
+ const i = field.interval({ fields: "year to month", precision: 2 }).finalize("span");
92
+ expect(i.sqlType).toBe("interval");
93
+ expect(i.typeOptions?.fields).toBe("year to month");
94
+ expect(i.typeOptions?.precision).toBe(2);
95
+ });
96
+
97
+ test("point / line default to tuple mode", () => {
98
+ const p = field.point().finalize("loc");
99
+ expect(p.typeOptions?.mode).toBe("tuple");
100
+ const xy = field.point({ mode: "xy" }).finalize("loc_xy");
101
+ expect(xy.typeOptions?.mode).toBe("xy");
102
+
103
+ const l = field.line().finalize("ln");
104
+ expect(l.typeOptions?.mode).toBe("tuple");
105
+ const abc = field.line({ mode: "abc" }).finalize("ln_abc");
106
+ expect(abc.typeOptions?.mode).toBe("abc");
107
+ });
108
+
109
+ test("network + binary + json factories record their sql type", () => {
110
+ expect(field.uuid().finalize("u").sqlType).toBe("uuid");
111
+ expect(field.bytea().finalize("blob_col").sqlType).toBe("bytea");
112
+ expect(field.json().finalize("meta").sqlType).toBe("json");
113
+ expect(field.jsonb().finalize("doc").sqlType).toBe("jsonb");
114
+ expect(field.inet().finalize("ip").sqlType).toBe("inet");
115
+ expect(field.cidr().finalize("net").sqlType).toBe("cidr");
116
+ expect(field.macaddr().finalize("mac").sqlType).toBe("macaddr");
117
+ expect(field.macaddr8().finalize("mac8").sqlType).toBe("macaddr8");
118
+ expect(field.boolean().finalize("active").sqlType).toBe("boolean");
119
+ expect(field.smallint().finalize("small").sqlType).toBe("smallint");
120
+ expect(field.real().finalize("r").sqlType).toBe("real");
121
+ expect(field.doublePrecision().finalize("d").sqlType).toBe("doublePrecision");
122
+ });
123
+
124
+ test(".type<T>() override preserves state and nullability", () => {
125
+ const branded = field.uuid().primaryKey().finalize("id") as unknown as ReturnType<
126
+ FieldBuilder<string, true>["type"]
127
+ >;
128
+ void branded;
129
+
130
+ const b = field.text();
131
+ const overridden = b.type<{ brand: "x" }>();
132
+ const col = (overridden as FieldBuilder<unknown, false>).notNull().finalize("thing");
133
+ expect(col.sqlType).toBe("text");
134
+ expect(col.notNull).toBe(true);
135
+ });
136
+
137
+ test(".okid() is a prepared id default — string columns only", () => {
138
+ const col = field.text().notNull().okid();
139
+ const decl = col.finalize("id");
140
+ expect(decl.defaultFnKind).toBe("id");
141
+ expect(typeof decl.defaultFn).toBe("function");
142
+ expect(String(decl.defaultFn!())).toMatch(/^[A-Za-z0-9-_]{21}$/);
143
+
144
+ // Non-string columns reject .okid() at the type level.
145
+ // @ts-expect-error — .okid() requires a string-typed column
146
+ field.integer().okid();
147
+ // @ts-expect-error — shared builder widened to unknown does not permit .okid()
148
+ (field.text() as FieldBuilder<unknown, false>).okid();
149
+ });
150
+
151
+ test("field.id() is a prepared default-generation-id text column", () => {
152
+ const col = field.id().primaryKey().finalize("id");
153
+ expect(col.sqlType).toBe("text");
154
+ expect(col.notNull).toBe(true);
155
+ expect(col.primaryKey).toBe(true);
156
+ expect(col.defaultFnKind).toBe("id");
157
+ expect(typeof col.defaultFn).toBe("function");
158
+ expect(String(col.defaultFn!())).toMatch(/^[A-Za-z0-9-_]{21}$/);
159
+ expect(emitColumnSource(col, "postgres")).toContain(".$defaultFn(id)");
160
+ });
161
+
162
+ test("field.okid() is an explicit OK-ID text column", () => {
163
+ const col = field.okid().primaryKey().finalize("id");
164
+ expect(col.sqlType).toBe("text");
165
+ expect(col.notNull).toBe(true);
166
+ expect(col.defaultFnKind).toBe("id");
167
+ expect(String(col.defaultFn!())).toMatch(/^[A-Za-z0-9-_]{21}$/);
168
+ expect(emitColumnSource(col, "postgres")).toContain(".$defaultFn(id)");
169
+
170
+ // Same emitted Drizzle as field.id() today (same generator).
171
+ const viaId = field.id().primaryKey().finalize("id");
172
+ expect(emitColumnSource(col, "postgres")).toBe(emitColumnSource(viaId, "postgres"));
173
+ });
174
+
175
+ test(".now() is a prepared default — resolved to the column's temporal mode", () => {
176
+ // number-shaped column → epoch-ms.
177
+ const col = field.integer().notNull().now();
178
+ const decl = col.finalize("at");
179
+ expect(decl.defaultFnKind).toBe("now");
180
+ expect(typeof decl.defaultFn).toBe("function");
181
+ const v = decl.defaultFn!();
182
+ expect(typeof v).toBe("number");
183
+ expect(v).toBeGreaterThan(0);
184
+
185
+ // timestamp (default date mode) → Date object.
186
+ const at = field.timestamp().notNull().now().finalize("seen_at");
187
+ expect(at.defaultFnKind).toBe("nowDate");
188
+ expect(at.defaultFn!() instanceof Date).toBe(true);
189
+
190
+ // timestamp ({ mode: "string" }) → ISO-8601 string.
191
+ const born = field.timestamp({ mode: "string" }).notNull().now().finalize("born_at");
192
+ expect(born.defaultFnKind).toBe("nowIso");
193
+ expect(typeof born.defaultFn!()).toBe("string");
194
+ });
195
+
196
+ test(".okid()/.now() chain through constraints and emit parity with defaultFn", () => {
197
+ const viaShorthand = field.text().primaryKey().okid().finalize("id");
198
+ const viaDefaultFn = field.text().primaryKey().defaultFn(id).finalize("id");
199
+ expect(viaShorthand.defaultFnKind).toBe(viaDefaultFn.defaultFnKind);
200
+ expect(viaShorthand.defaultFnKind).toBe("id");
201
+ expect(emitColumnSource(viaShorthand, "postgres")).toBe(
202
+ emitColumnSource(viaDefaultFn, "postgres"),
203
+ );
204
+
205
+ const atShorthand = field.integer().notNull().now().finalize("created_ms");
206
+ expect(atShorthand.defaultFnKind).toBe("now");
207
+ expect(emitColumnSource(atShorthand, "postgres")).toContain(".$defaultFn(now)");
208
+
209
+ const tsShorthand = field.timestamp().notNull().now().finalize("created_at");
210
+ expect(tsShorthand.defaultFnKind).toBe("nowDate");
211
+ expect(emitColumnSource(tsShorthand, "postgres")).toContain(".$defaultFn(nowDate)");
212
+
213
+ const dateShorthand = field.timestamp({ mode: "string" }).notNull().now().finalize("born_at");
214
+ expect(dateShorthand.defaultFnKind).toBe("nowIso");
215
+ expect(emitColumnSource(dateShorthand, "postgres")).toContain(".$defaultFn(nowIso)");
216
+ });
217
+
218
+ test("options survive schemaTable finalize", () => {
219
+ const events = store.schema.table("events", {
220
+ id: field.uuid().primaryKey().defaultFn(id),
221
+ kind: field.varchar({ length: 64 }).notNull(),
222
+ payload: field.jsonb(),
223
+ at: field.timestamp({ precision: 6, withTimezone: true }).notNull(),
224
+ amount: field.numeric({ precision: 12, scale: 4 }),
225
+ });
226
+ expect(events.columns.kind.typeOptions?.length).toBe(64);
227
+ expect(events.columns.at.typeOptions).toEqual({
228
+ precision: 6,
229
+ withTimezone: true,
230
+ mode: "date",
231
+ });
232
+ expect(events.columns.amount.typeOptions).toEqual({ precision: 12, scale: 4 });
233
+ });
234
+ });
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Drizzle emitter — per-type source
238
+ // ---------------------------------------------------------------------------
239
+
240
+ describe("emitColumnSource — per-type drizzle-exact output", () => {
241
+ function src(col: Parameters<typeof emitColumnSource>[0]): string {
242
+ return emitColumnSource(col);
243
+ }
244
+
245
+ test("integer widens to bigint with explicit number mode", () => {
246
+ expect(src(store.schema.table("t", { n: field.integer() }).columns.n!)).toBe(
247
+ 'bigint("n", { mode: "number" })',
248
+ );
249
+ });
250
+
251
+ test("varchar emits length config", () => {
252
+ expect(src(field.varchar({ length: 256 }).finalize("slug"))).toBe(
253
+ 'varchar("slug", { length: 256 })',
254
+ );
255
+ });
256
+
257
+ test("text enum emits enum config", () => {
258
+ expect(src(field.text({ enum: ["a", "b"] as const }).finalize("status"))).toBe(
259
+ 'text("status", { enum: ["a","b"] })',
260
+ );
261
+ });
262
+
263
+ test("timestamp always pins mode explicitly", () => {
264
+ expect(src(field.timestamp().finalize("created"))).toBe(
265
+ 'timestamp("created", { mode: "date" })',
266
+ );
267
+ expect(src(field.timestamp({ mode: "string" }).finalize("born"))).toBe(
268
+ 'timestamp("born", { mode: "string" })',
269
+ );
270
+ expect(
271
+ src(field.timestamp({ mode: "date", precision: 6, withTimezone: true }).finalize("seen")),
272
+ ).toBe('timestamp("seen", { mode: "date", precision: 6, withTimezone: true })');
273
+ });
274
+
275
+ test("date always pins mode explicitly", () => {
276
+ expect(src(field.date().finalize("day"))).toBe('date("day", { mode: "date" })');
277
+ expect(src(field.date({ mode: "string" }).finalize("bd"))).toBe(
278
+ 'date("bd", { mode: "string" })',
279
+ );
280
+ });
281
+
282
+ test("numeric emits precision/scale/mode via config object", () => {
283
+ expect(src(field.numeric({ precision: 10, scale: 2 }).finalize("amount"))).toBe(
284
+ 'numeric("amount", { precision: 10, scale: 2 })',
285
+ );
286
+ expect(src(field.numeric({ mode: "number" }).finalize("m")).trim()).toContain('mode: "number"');
287
+ });
288
+
289
+ test("bigserial emits explicit mode; serial stays bare", () => {
290
+ // Serial family is NOT NULL by SQL physics — emitter appends .notNull().
291
+ expect(src(field.bigserial({ mode: "bigint" }).finalize("bs"))).toBe(
292
+ 'bigserial("bs", { mode: "bigint" }).notNull()',
293
+ );
294
+ expect(src(field.serial().finalize("s"))).toBe('serial("s").notNull()');
295
+ });
296
+
297
+ test("geometric modes emit only when non-default", () => {
298
+ expect(src(field.point({ mode: "xy" }).finalize("p"))).toBe('point("p", { mode: "xy" })');
299
+ expect(src(field.point().finalize("p2"))).toBe('point("p2")');
300
+ expect(src(field.line({ mode: "abc" }).finalize("l"))).toBe('line("l", { mode: "abc" })');
301
+ });
302
+
303
+ test("time / interval configs pass through", () => {
304
+ expect(src(field.time({ withTimezone: true, precision: 3 }).finalize("at"))).toBe(
305
+ 'time("at", { withTimezone: true, precision: 3 })',
306
+ );
307
+ expect(src(field.interval({ fields: "day to second", precision: 4 }).finalize("span"))).toBe(
308
+ 'interval("span", { fields: "day to second", precision: 4 })',
309
+ );
310
+ });
311
+
312
+ test("bytea / uuid / network families emit bare calls", () => {
313
+ expect(src(field.bytea().finalize("bin"))).toBe('bytea("bin")');
314
+ expect(src(field.inet().finalize("ip"))).toBe('inet("ip")');
315
+ expect(src(field.macaddr8().finalize("m8"))).toBe('macaddr8("m8")');
316
+ expect(src(field.doublePrecision().finalize("d"))).toBe('doublePrecision("d")');
317
+ expect(src(field.boolean().finalize("b"))).toBe('boolean("b")');
318
+ expect(src(field.jsonb().finalize("j"))).toBe('jsonb("j")');
319
+ });
320
+
321
+ test("Date and Buffer defaults serialize as executable literals", () => {
322
+ expect(
323
+ src(
324
+ store.schema.table("t", {
325
+ d: field.date({ mode: "date" }).default(new Date("2025-01-01T00:00:00.000Z")),
326
+ }).columns.d!,
327
+ ),
328
+ ).toBe('date("d", { mode: "date" }).default(new Date("2025-01-01T00:00:00.000Z"))');
329
+ expect(
330
+ src(store.schema.table("t", { b: field.bytea().default(Buffer.from("hi")) }).columns.b!),
331
+ ).toBe(
332
+ `bytea("b").default(Buffer.from(${JSON.stringify(Buffer.from("hi").toString("base64"))}, "base64"))`,
333
+ );
334
+ });
335
+
336
+ test("pg-core import list collects used factories dynamically", () => {
337
+ const events = store.schema.table("events", {
338
+ id: field.uuid().primaryKey().defaultFn(id),
339
+ doc: field.jsonb(),
340
+ at: field.timestamp({ mode: "date" }).notNull(),
341
+ });
342
+ const out = emitDrizzleSource([events], "postgres");
343
+ expect(out).toContain('import { pgTable, uuid, jsonb, timestamp } from "drizzle-orm/pg-core"');
344
+ expect(out).not.toContain(" bigint,");
345
+ });
346
+
347
+ test("emitted source loads against real drizzle-orm rc.5 factories", async () => {
348
+ const events = store.schema.table("events", {
349
+ id: field.uuid().primaryKey().defaultFn(id),
350
+ status: field.text({ enum: ["new", "archived"] as const }),
351
+ slug: field.varchar({ length: 128 }).notNull().unique(),
352
+ doc: field.jsonb(),
353
+ at: field.timestamp({ mode: "date", withTimezone: true }).notNull(),
354
+ day: field.date(),
355
+ amount: field.numeric({ precision: 12, scale: 2 }),
356
+ ratio: field.numeric({ mode: "number" }),
357
+ span: field.interval({ fields: "day to second" }),
358
+ loc: field.point({ mode: "xy" }),
359
+ ip: field.inet(),
360
+ bin: field.bytea(),
361
+ big: field.bigserial({ mode: "bigint" }),
362
+ small: field.smallint(),
363
+ exact: field.doublePrecision(),
364
+ flag: field.boolean(),
365
+ });
366
+ const out = emitDrizzleSource([events], "postgres");
367
+ // Write under the repo's gitignored .oke/ scratch so `drizzle-orm/pg-core`
368
+ // resolves through workspace node_modules.
369
+ const { mkdirSync, writeFileSync, rmSync } = await import("node:fs");
370
+ const path = await import("node:path");
371
+ const dir = path.join(".oke", "emit-test", crypto.randomUUID().slice(0, 8));
372
+ mkdirSync(dir, { recursive: true });
373
+ const file = path.join(dir, "schema.drizzle.ts");
374
+ writeFileSync(file, out);
375
+ try {
376
+ const mod = (await import(path.resolve(file))) as Record<string, unknown>;
377
+ expect(Object.keys(mod)).toContain("events");
378
+ const tableObj = mod.events as Record<string, unknown>;
379
+ expect(Object.keys(tableObj)).toEqual(
380
+ expect.arrayContaining(["id", "status", "slug", "doc", "at", "amount"]),
381
+ );
382
+ } finally {
383
+ rmSync(path.dirname(dir), { recursive: true, force: true });
384
+ }
385
+ });
386
+ });
387
+
388
+ // ---------------------------------------------------------------------------
389
+ // Runtime auto-DDL — shared mapper
390
+ // ---------------------------------------------------------------------------
391
+
392
+ describe("ddlTypeOf — unparameterized PG DDL names", () => {
393
+ test("int family maps per width rules", () => {
394
+ expect(ddlTypeOf("smallint")).toBe("INTEGER");
395
+ expect(ddlTypeOf("smallserial")).toBe("INTEGER");
396
+ expect(ddlTypeOf("integer")).toBe("BIGINT");
397
+ expect(ddlTypeOf("bigint")).toBe("BIGINT");
398
+ expect(ddlTypeOf("serial")).toBe("BIGINT");
399
+ expect(ddlTypeOf("bigserial")).toBe("BIGINT");
400
+ });
401
+
402
+ test("float family splits REAL vs DOUBLE PRECISION vs NUMERIC", () => {
403
+ expect(ddlTypeOf("real")).toBe("REAL");
404
+ expect(ddlTypeOf("doublePrecision")).toBe("DOUBLE PRECISION");
405
+ expect(ddlTypeOf("numeric")).toBe("NUMERIC");
406
+ });
407
+
408
+ test("json/jsonb collapse to JSONB; bytea/geometric/temporals map", () => {
409
+ expect(ddlTypeOf("json")).toBe("JSONB");
410
+ expect(ddlTypeOf("jsonb")).toBe("JSONB");
411
+ expect(ddlTypeOf("bytea")).toBe("BYTEA");
412
+ expect(ddlTypeOf("point")).toBe("POINT");
413
+ expect(ddlTypeOf("line")).toBe("LINE");
414
+ expect(ddlTypeOf("timestamp")).toBe("TIMESTAMP");
415
+ expect(ddlTypeOf("date")).toBe("DATE");
416
+ });
417
+
418
+ test("text-shaped types fall back to TEXT", () => {
419
+ for (const t of [
420
+ "text",
421
+ "varchar",
422
+ "char",
423
+ "uuid",
424
+ "time",
425
+ "interval",
426
+ "inet",
427
+ "cidr",
428
+ "macaddr",
429
+ "macaddr8",
430
+ ]) {
431
+ expect(ddlTypeOf(t)).toBe("TEXT");
432
+ }
433
+ expect(ddlTypeOf("something-new")).toBe("TEXT");
434
+ });
435
+
436
+ test("resolveColumns consumes the mapper on TableHandles", () => {
437
+ const handle = {
438
+ name: "mixed",
439
+ columns: {
440
+ id: { name: "id" },
441
+ count: { name: "count", sqlType: "integer" },
442
+ active: { name: "active", sqlType: "boolean" },
443
+ doc: { name: "doc", sqlType: "jsonb" },
444
+ at: { name: "at", sqlType: "timestamp" },
445
+ },
446
+ };
447
+ const cols = resolveColumns(handle) as ResolvedColumn[];
448
+ expect(cols.map((c) => c.sqlName)).toEqual(["id", "count", "active", "doc", "at"]);
449
+ expect(cols.find((c) => c.sqlName === "id")!.sqlType).toBe("TEXT");
450
+ expect(cols.find((c) => c.sqlName === "count")!.sqlType).toBe("BIGINT");
451
+ expect(cols.find((c) => c.sqlName === "active")!.sqlType).toBe("BOOLEAN");
452
+ expect(cols.find((c) => c.sqlName === "doc")!.sqlType).toBe("JSONB");
453
+ expect(cols.find((c) => c.sqlName === "at")!.sqlType).toBe("TIMESTAMP");
454
+ });
455
+ });