okengine 0.11.0 → 0.11.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.
Files changed (68) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/elements/ai.mdx +22 -1
  3. package/site/content/docs/elements/store.mdx +3 -1
  4. package/site/content/docs/elements/vault.mdx +19 -11
  5. package/site/content/docs/get-started/installation.mdx +7 -1
  6. package/site/content/docs/recipes/llama-cpp.mdx +10 -9
  7. package/site/content/docs/reference/cli.md +6 -2
  8. package/site/content/docs/reference/environment-variables.mdx +9 -9
  9. package/src/cli/ai-setup/ai-setup.test.ts +3 -1
  10. package/src/cli/ai-setup/apply.ts +61 -1
  11. package/src/cli/ask-seed.test.ts +4 -3
  12. package/src/cli/ask-seed.ts +5 -6
  13. package/src/cli/client-add.test.ts +2 -1
  14. package/src/cli/dev.test.ts +116 -0
  15. package/src/cli/dev.ts +107 -18
  16. package/src/cli/project-state.test.ts +50 -0
  17. package/src/cli/project-state.ts +123 -0
  18. package/src/cli/vault-cmd.test.ts +47 -18
  19. package/src/cli/vault-cmd.ts +2 -1
  20. package/src/compiler/extract.ts +12 -1
  21. package/src/console/server/console.test.ts +3 -1
  22. package/src/console/server/operator-db.test.ts +48 -17
  23. package/src/console/server/operator-db.ts +5 -1
  24. package/src/docker/derive.ts +24 -3
  25. package/src/docker/docker.test.ts +4 -2
  26. package/src/docker/index.ts +1 -0
  27. package/src/docker/recipes/index.ts +1 -0
  28. package/src/docker/recipes/llama-cpp.ts +20 -4
  29. package/src/drivers/ai-openai-compatible.ts +15 -3
  30. package/src/drivers/vault-builtin.test.ts +50 -42
  31. package/src/elements/ai/declare.ts +73 -3
  32. package/src/elements/ai/errors.test.ts +35 -0
  33. package/src/elements/ai/errors.ts +139 -0
  34. package/src/elements/ai/eval.ts +26 -1
  35. package/src/elements/ai/runtime.ts +140 -80
  36. package/src/elements/ai/tools.test.ts +1 -1
  37. package/src/elements/ai.test.ts +99 -2
  38. package/src/elements/ai.ts +11 -1
  39. package/src/elements/gate/config.ts +13 -3
  40. package/src/elements/gate/declare.ts +1 -1
  41. package/src/elements/gate/strategies.ts +2 -12
  42. package/src/elements/index.ts +2 -0
  43. package/src/elements/store/index-boot.test.ts +23 -6
  44. package/src/elements/store/resource.test.ts +38 -19
  45. package/src/elements/store/sql-session.test.ts +55 -58
  46. package/src/elements/vault/builtin-adapter.test.ts +115 -58
  47. package/src/elements/vault/builtin-adapter.ts +241 -47
  48. package/src/elements/vault/chaos-child.ts +424 -0
  49. package/src/elements/vault/chaos.test.ts +651 -0
  50. package/src/elements/vault/resilience.ts +6 -1
  51. package/src/elements/vault/security-checklist.test.ts +10 -8
  52. package/src/elements/vault/storage.ts +130 -27
  53. package/src/elements/vault/test-helpers.ts +368 -0
  54. package/src/elements/vault.ts +6 -0
  55. package/src/index.ts +2 -0
  56. package/src/kernel/app-auth.ts +98 -0
  57. package/src/kernel/app.ts +120 -78
  58. package/src/kernel/auto-registry.test.ts +52 -1
  59. package/src/kernel/boot.test.ts +4 -18
  60. package/src/kernel/element-registries.ts +19 -4
  61. package/src/kernel/errors.ts +3 -3
  62. package/src/kernel/fx.test.ts +25 -0
  63. package/src/kernel/fx.ts +4 -1
  64. package/src/manifest/types.ts +4 -0
  65. package/src/release/build-lib.ts +3 -0
  66. package/src/shared/lazy-src.ts +79 -0
  67. package/src/test/create-test-app.ts +16 -11
  68. package/src/test/reset-element-registries.ts +17 -9
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * `oke({ gate })` — nested Gate bag (auth · policies · rate · posture).
3
+ *
4
+ * Auth resolution is sync-lazy via {@link requirePackageModule} so HTTP-only
5
+ * apps never evaluate `auth/config` (and so `dist/` can ship a separate chunk).
3
6
  */
4
7
 
5
- import { resolveGateAuth, type GateAuthOptions, type ResolvedGateAuth } from "../../auth/config.ts";
8
+ import type { GateAuthOptions, ResolvedGateAuth } from "../../auth/config.ts";
9
+ import { requirePackageModule } from "../../shared/lazy-src.ts";
6
10
  import type { GateDecl } from "./declare.ts";
7
11
 
8
12
  /** Rate-limit defaults under Gate. */
@@ -51,8 +55,14 @@ export interface ResolveGateConfigOptions {
51
55
  */
52
56
  export function resolveGateConfig(options: ResolveGateConfigOptions = {}): ResolvedGateConfig {
53
57
  const bag = options.gate ?? {};
54
- const auth =
55
- bag.auth !== undefined ? resolveGateAuth({ auth: bag.auth, env: options.env }) : undefined;
58
+ let auth: ResolvedGateAuth | undefined;
59
+ if (bag.auth !== undefined) {
60
+ const { resolveGateAuth } = requirePackageModule<typeof import("../../auth/config.ts")>(
61
+ "auth/config",
62
+ "auth-config",
63
+ );
64
+ auth = resolveGateAuth({ auth: bag.auth, env: options.env });
65
+ }
56
66
  const rateLimitEnabled =
57
67
  bag.rateLimit?.enabled !== undefined
58
68
  ? bag.rateLimit.enabled
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { RateStrategy } from "../../manifest/types.ts";
8
- import { DEFAULT_RATE_STRATEGY } from "./strategies.ts";
8
+ import { DEFAULT_RATE_STRATEGY } from "./constants.ts";
9
9
 
10
10
  /** Context passed to policy predicates at evaluation time. */
11
11
  export interface GatePolicyContext {
@@ -11,6 +11,8 @@
11
11
  import { registerLuaScript, type LuaKvStore } from "../../drivers/kv-lua.ts";
12
12
  import type { RateStrategy } from "../../manifest/types.ts";
13
13
 
14
+ export { ALL_RATE_STRATEGIES, DEFAULT_RATE_STRATEGY } from "./constants.ts";
15
+
14
16
  /** Result of a rate-limit take attempt. */
15
17
  export interface RateTakeResult {
16
18
  /** Whether the request is allowed. */
@@ -338,18 +340,6 @@ export const RATE_STRATEGIES: Record<RateStrategy, StrategyDef> = {
338
340
  "leaky-bucket": leakyBucket,
339
341
  };
340
342
 
341
- /** Default rate strategy (unified-theory §16). */
342
- export const DEFAULT_RATE_STRATEGY: RateStrategy = "sliding-window-counter";
343
-
344
- /** All five strategy ids. */
345
- export const ALL_RATE_STRATEGIES: readonly RateStrategy[] = [
346
- "fixed-window",
347
- "sliding-window-counter",
348
- "sliding-log",
349
- "token-bucket",
350
- "leaky-bucket",
351
- ];
352
-
353
343
  for (const def of Object.values(RATE_STRATEGIES)) {
354
344
  registerLuaScript(def.lua, (store, keys, args) => def.run(store, keys, args));
355
345
  }
@@ -163,6 +163,8 @@ export type {
163
163
 
164
164
  export {
165
165
  ai,
166
+ listAiDecls,
167
+ resetAiDecls,
166
168
  createAiRuntime,
167
169
  assertAllowPiiForAsk,
168
170
  AiPiiBuildError,
@@ -6,7 +6,7 @@
6
6
  * native ANN index — no JS-side full scan survives.
7
7
  */
8
8
 
9
- import { afterEach, describe, expect, test } from "bun:test";
9
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
10
10
  import { connectPglite } from "../../drivers/pglite.ts";
11
11
  import { openPgvectorIndex } from "../../drivers/pgvector.ts";
12
12
  import { connectPostgres } from "../../drivers/postgres.ts";
@@ -22,6 +22,17 @@ const prev = {
22
22
  pglite: process.env.OKE_PGLITE_URL,
23
23
  };
24
24
 
25
+ /** Warmed PGlite for direct openPgvectorIndex cases (boot path opens its own). */
26
+ let sharedPglite: SqlConnection | undefined;
27
+
28
+ beforeAll(async () => {
29
+ sharedPglite = await connectPglite({ url: "memory://index-boot-shared" });
30
+ }, 15_000);
31
+
32
+ afterAll(async () => {
33
+ await sharedPglite?.close();
34
+ });
35
+
25
36
  afterEach(() => {
26
37
  if (prev.pglite === undefined) delete process.env.OKE_PGLITE_URL;
27
38
  else process.env.OKE_PGLITE_URL = prev.pglite;
@@ -43,7 +54,8 @@ function recordingConn(conn: SqlConnection): { conn: SqlConnection; statements:
43
54
  statements.push(sql);
44
55
  return conn.exec(sql, params);
45
56
  },
46
- close: () => conn.close(),
57
+ // Never close the underlying shared PGlite — afterAll owns that.
58
+ close: async () => {},
47
59
  },
48
60
  };
49
61
  }
@@ -63,12 +75,16 @@ describe("store.index boot wiring — memory", () => {
63
75
  });
64
76
 
65
77
  describe("store.index boot wiring — pglite + pgvector", () => {
78
+ // bindStore opens its own PGlite — cannot inject the shared instance; allow
79
+ // cold WASM under suite contention (same budget as sql conformance).
66
80
  test("boot: sql=pglite + index=pgvector resolves end to end", async () => {
67
81
  process.env.OKE_PGLITE_URL = "memory://";
68
82
  const kb = declareIndex("kb", { dims: 3 });
69
83
  const runtime = bindStore(
70
84
  {
71
- config: { drivers: { store: { sql: { test: "pglite" }, index: { test: "pgvector" } } } },
85
+ config: {
86
+ drivers: { store: { sql: { test: "pglite" }, index: { test: "pgvector" } } },
87
+ },
72
88
  stores: [kb],
73
89
  },
74
90
  "test",
@@ -86,10 +102,11 @@ describe("store.index boot wiring — pglite + pgvector", () => {
86
102
  expect(hits[0]!.meta).toEqual({ label: "near" });
87
103
  expect(await handle.delete("farther")).toBe(true);
88
104
  await runtime.close();
89
- });
105
+ }, 15_000);
90
106
 
91
107
  test("pglite runs the shared pgvector HNSW path — real ANN, not a scan", async () => {
92
- const real = await connectPglite({ url: "memory://" });
108
+ const real = sharedPglite!;
109
+ await real.exec(`DROP TABLE IF EXISTS oke_idx_ann CASCADE`);
93
110
  const { conn, statements } = recordingConn(real);
94
111
  const idx = await openPgvectorIndex({ name: "ann", dims: 3, sql: conn });
95
112
  await idx.upsert("a", [1, 0, 0]);
@@ -106,7 +123,7 @@ describe("store.index boot wiring — pglite + pgvector", () => {
106
123
  );
107
124
  expect(JSON.stringify(indexes)).toContain("USING hnsw");
108
125
  await idx.close();
109
- await real.close();
126
+ // shared connection — closed in afterAll
110
127
  });
111
128
  });
112
129
 
@@ -5,18 +5,19 @@
5
5
  * client-facing key.
6
6
  */
7
7
 
8
- import { afterEach, describe, expect, test } from "bun:test";
8
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
9
9
  import { integer, pgTable, text } from "drizzle-orm/pg-core";
10
10
  import { createInsertSchema, createSelectSchema } from "drizzle-zod";
11
11
  import { z } from "zod";
12
12
  import { pgliteDriver } from "../../drivers/pglite.ts";
13
+ import type { SqlConnection } from "../../drivers/types.ts";
13
14
  import { oke } from "../../kernel/app.ts";
14
15
  import { resetFlowSeq } from "../../kernel/flow.ts";
15
16
  import { on, resetBindings } from "../../kernel/on.ts";
16
17
  import { http } from "../../kernel/triggers.ts";
17
18
  import { createTestApp } from "../../test/create-test-app.ts";
18
19
  import { classify, field, id, now, PII_MASK, store } from "../store.ts";
19
- import { createSqlStoreHandle } from "./sql-session.ts";
20
+ import { createSqlStoreHandle, type SqlStoreHandle } from "./sql-session.ts";
20
21
  import { mapRowToJs, resolveColumns } from "./table.ts";
21
22
 
22
23
  afterEach(() => {
@@ -24,6 +25,18 @@ afterEach(() => {
24
25
  resetFlowSeq();
25
26
  });
26
27
 
28
+ /** Posts table for the JS-key exit lock (needs real Postgres dialect via PGlite). */
29
+ const jsKeyPosts = pgTable("posts", {
30
+ id: text("id").primaryKey(),
31
+ title: text("title").notNull(),
32
+ createdAt: integer("created_at").notNull(),
33
+ });
34
+
35
+ /** File-scoped warmed PGlite for the SqlStoreHandle exit describe. */
36
+ let sharedConn: SqlConnection;
37
+ /** Handle over {@link sharedConn}. */
38
+ let sharedHandle: SqlStoreHandle;
39
+
27
40
  describe("mapRowToJs — declared keys only", () => {
28
41
  const posts = pgTable("posts", {
29
42
  id: text("id").primaryKey(),
@@ -47,40 +60,46 @@ describe("mapRowToJs — declared keys only", () => {
47
60
  });
48
61
 
49
62
  describe("SqlStoreHandle exit — list/get/create return JS keys only", () => {
50
- const posts = pgTable("posts", {
51
- id: text("id").primaryKey(),
52
- title: text("title").notNull(),
53
- createdAt: integer("created_at").notNull(),
54
- });
55
-
56
- test("select / findById / insert.returning never emit created_at", async () => {
57
- const conn = await pgliteDriver.connect({
58
- url: "memory://",
63
+ // Real Postgres dialect (PGlite) — share one warmed instance; TRUNCATE between tests.
64
+ beforeAll(async () => {
65
+ sharedConn = await pgliteDriver.connect({
66
+ url: "memory://resource-js-keys-shared",
59
67
  role: "primary",
60
68
  });
61
- const handle = createSqlStoreHandle("sql:app", {
62
- connection: conn,
69
+ sharedHandle = createSqlStoreHandle("sql:app", {
70
+ connection: sharedConn,
63
71
  classifications: new Map(),
64
72
  routedRole: "primary",
65
73
  domainDdl: "ensure",
66
74
  });
75
+ // Warm DDL via the insert path (pgTable is not a TableHandle for ensureTable).
76
+ await sharedHandle.insert(jsKeyPosts).values({ id: "_warmup", title: "x", createdAt: 0 });
77
+ await sharedConn.exec(`TRUNCATE "posts" RESTART IDENTITY CASCADE`);
78
+ }, 15_000);
79
+
80
+ afterAll(async () => {
81
+ await sharedConn.close();
82
+ });
67
83
 
68
- const [created] = await handle
69
- .insert(posts)
84
+ beforeEach(async () => {
85
+ await sharedConn.exec(`TRUNCATE "posts" RESTART IDENTITY CASCADE`);
86
+ });
87
+
88
+ test("select / findById / insert.returning never emit created_at", async () => {
89
+ const [created] = await sharedHandle
90
+ .insert(jsKeyPosts)
70
91
  .values({ id: "p1", title: "one", createdAt: 100 })
71
92
  .returning();
72
93
  expect(Object.keys(created!).sort()).toEqual(["createdAt", "id", "title"]);
73
94
  expect("created_at" in created!).toBe(false);
74
95
 
75
- const listed = await handle.select().from(posts);
96
+ const listed = await sharedHandle.select().from(jsKeyPosts);
76
97
  expect(Object.keys(listed[0]!).sort()).toEqual(["createdAt", "id", "title"]);
77
98
  expect("created_at" in listed[0]!).toBe(false);
78
99
 
79
- const got = await handle.findById(posts, "p1");
100
+ const got = await sharedHandle.findById(jsKeyPosts, "p1");
80
101
  expect(Object.keys(got!).sort()).toEqual(["createdAt", "id", "title"]);
81
102
  expect("created_at" in got!).toBe(false);
82
-
83
- await conn.close();
84
103
  });
85
104
  });
86
105
 
@@ -4,9 +4,13 @@
4
4
  * Relational `with:` / Drizzle RQB (`db.query.*.findMany({ with })`) is a
5
5
  * documented limitation: effects, cache keys, and PII masking assume one
6
6
  * table per call, so no relational query surface may appear on the handle.
7
+ *
8
+ * Upsert / orderBy / like need the real Postgres dialect (PGlite). One warmed
9
+ * in-memory PGlite is shared for the whole file (cold WASM once); TRUNCATE
10
+ * isolates each test without a fresh `PGlite.create`.
7
11
  */
8
12
 
9
- import { describe, expect, test } from "bun:test";
13
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
10
14
  import { and, desc, eq, like, lt, or } from "drizzle-orm";
11
15
  import { integer, pgTable, text } from "drizzle-orm/pg-core";
12
16
  import { pgliteDriver } from "../../drivers/pglite.ts";
@@ -19,31 +23,38 @@ const posts = pgTable("posts", {
19
23
  createdAt: integer("created_at").notNull(),
20
24
  });
21
25
 
22
- async function openHandle(): Promise<{ handle: SqlStoreHandle; conn: SqlConnection }> {
23
- const conn = await pgliteDriver.connect({
24
- url: "memory://",
26
+ /** File-scoped warmed PGlite opened in {@link beforeAll}. */
27
+ let sharedConn: SqlConnection;
28
+ /** Handle over {@link sharedConn}. */
29
+ let sharedHandle: SqlStoreHandle;
30
+
31
+ beforeAll(async () => {
32
+ sharedConn = await pgliteDriver.connect({
33
+ url: "memory://sql-session-shared",
25
34
  role: "primary",
26
35
  });
27
- const handle = createSqlStoreHandle("sql:app", {
28
- connection: conn,
36
+ sharedHandle = createSqlStoreHandle("sql:app", {
37
+ connection: sharedConn,
29
38
  classifications: new Map(),
30
39
  routedRole: "primary",
31
40
  domainDdl: "ensure",
32
41
  });
33
- return { handle, conn };
34
- }
42
+ // Warm DDL via the insert path (pgTable is not a TableHandle for ensureTable).
43
+ await sharedHandle.insert(posts).values({ id: "_warmup", title: "x", createdAt: 0 });
44
+ await sharedConn.exec(`TRUNCATE "posts" RESTART IDENTITY CASCADE`);
45
+ }, 15_000);
46
+
47
+ afterAll(async () => {
48
+ await sharedConn.close();
49
+ });
50
+
51
+ beforeEach(async () => {
52
+ await sharedConn.exec(`TRUNCATE "posts" RESTART IDENTITY CASCADE`);
53
+ });
35
54
 
36
55
  describe("SqlStoreHandle — no relational query surface (path b)", () => {
37
- test("created handle exposes exactly the single-table surface", async () => {
38
- const conn = await pgliteDriver.connect({ url: "memory://", role: "primary" });
39
- const handle = createSqlStoreHandle("sql:app", {
40
- connection: conn,
41
- classifications: new Map(),
42
- routedRole: "primary",
43
- domainDdl: "ensure",
44
- });
45
-
46
- expect(Object.keys(handle).sort()).toEqual(
56
+ test("created handle exposes exactly the single-table surface", () => {
57
+ expect(Object.keys(sharedHandle).sort()).toEqual(
47
58
  [
48
59
  "ref",
49
60
  "routedRole",
@@ -64,47 +75,40 @@ describe("SqlStoreHandle — no relational query surface (path b)", () => {
64
75
  );
65
76
 
66
77
  // No Drizzle RQB / with: surface — ever.
67
- expect("query" in handle).toBe(false);
68
- expect("findMany" in handle).toBe(false);
69
- expect("findFirst" in handle).toBe(false);
70
- expect("with" in handle).toBe(false);
71
-
72
- await conn.close();
78
+ expect("query" in sharedHandle).toBe(false);
79
+ expect("findMany" in sharedHandle).toBe(false);
80
+ expect("findFirst" in sharedHandle).toBe(false);
81
+ expect("with" in sharedHandle).toBe(false);
73
82
  });
74
83
  });
75
84
 
76
85
  describe("SqlStoreHandle — orderBy / limit select chain", () => {
77
86
  test("orderBy + limit without where", async () => {
78
- const { handle, conn } = await openHandle();
79
- await handle.insert(posts).values({ id: "p1", title: "one", createdAt: 100 });
80
- await handle.insert(posts).values({ id: "p2", title: "two", createdAt: 300 });
81
- await handle.insert(posts).values({ id: "p3", title: "three", createdAt: 200 });
87
+ await sharedHandle.insert(posts).values({ id: "p1", title: "one", createdAt: 100 });
88
+ await sharedHandle.insert(posts).values({ id: "p2", title: "two", createdAt: 300 });
89
+ await sharedHandle.insert(posts).values({ id: "p3", title: "three", createdAt: 200 });
82
90
 
83
- const rows = await handle.select().from(posts).orderBy(desc(posts.createdAt)).limit(2);
91
+ const rows = await sharedHandle.select().from(posts).orderBy(desc(posts.createdAt)).limit(2);
84
92
  expect(rows.map((r) => r.id)).toEqual(["p2", "p3"]);
85
- await conn.close();
86
93
  });
87
94
 
88
95
  test("limit directly on from() — no where required", async () => {
89
- const { handle, conn } = await openHandle();
90
- await handle.insert(posts).values({ id: "p1", title: "one", createdAt: 100 });
91
- await handle.insert(posts).values({ id: "p2", title: "two", createdAt: 300 });
96
+ await sharedHandle.insert(posts).values({ id: "p1", title: "one", createdAt: 100 });
97
+ await sharedHandle.insert(posts).values({ id: "p2", title: "two", createdAt: 300 });
92
98
 
93
- const rows = await handle.select().from(posts).limit(1);
99
+ const rows = await sharedHandle.select().from(posts).limit(1);
94
100
  expect(rows).toHaveLength(1);
95
- await conn.close();
96
101
  });
97
102
 
98
103
  test("composite cursor predicate: where → orderBy → limit", async () => {
99
- const { handle, conn } = await openHandle();
100
- await handle.insert(posts).values({ id: "p1", title: "one", createdAt: 100 });
101
- await handle.insert(posts).values({ id: "p2", title: "two", createdAt: 300 });
102
- await handle.insert(posts).values({ id: "p3", title: "three", createdAt: 200 });
104
+ await sharedHandle.insert(posts).values({ id: "p1", title: "one", createdAt: 100 });
105
+ await sharedHandle.insert(posts).values({ id: "p2", title: "two", createdAt: 300 });
106
+ await sharedHandle.insert(posts).values({ id: "p3", title: "three", createdAt: 200 });
103
107
  // Tie on createdAt=200 with an id that sorts after the cursor id.
104
- await handle.insert(posts).values({ id: "p0", title: "four", createdAt: 200 });
108
+ await sharedHandle.insert(posts).values({ id: "p0", title: "four", createdAt: 200 });
105
109
 
106
110
  const order = [desc(posts.createdAt), desc(posts.id)] as const;
107
- const page1 = await handle
111
+ const page1 = await sharedHandle
108
112
  .select()
109
113
  .from(posts)
110
114
  .orderBy(...order)
@@ -112,61 +116,54 @@ describe("SqlStoreHandle — orderBy / limit select chain", () => {
112
116
  expect(page1.map((r) => r.id)).toEqual(["p2", "p3"]);
113
117
 
114
118
  // (createdAt, id) < (200, "p3") — the second page of a keyset cursor.
115
- const page2 = await handle
119
+ const page2 = await sharedHandle
116
120
  .select()
117
121
  .from(posts)
118
122
  .where(or(lt(posts.createdAt, 200), and(eq(posts.createdAt, 200), lt(posts.id, "p3"))))
119
123
  .orderBy(...order)
120
124
  .limit(2);
121
125
  expect(page2.map((r) => r.id)).toEqual(["p0", "p1"]);
122
- await conn.close();
123
126
  });
124
127
 
125
128
  test("like filters rows through the session (postgres case-sensitive)", async () => {
126
- const { handle, conn } = await openHandle();
127
- await handle.insert(posts).values({ id: "p1", title: "alpha", createdAt: 100 });
128
- await handle.insert(posts).values({ id: "p2", title: "bravo", createdAt: 200 });
129
+ await sharedHandle.insert(posts).values({ id: "p1", title: "alpha", createdAt: 100 });
130
+ await sharedHandle.insert(posts).values({ id: "p2", title: "bravo", createdAt: 200 });
129
131
 
130
- const rows = await handle.select().from(posts).where(like(posts.title, "a%"));
132
+ const rows = await sharedHandle.select().from(posts).where(like(posts.title, "a%"));
131
133
  expect(rows.map((r) => r.id)).toEqual(["p1"]);
132
- await conn.close();
133
134
  });
134
135
  });
135
136
 
136
137
  describe("SqlStoreHandle — upsert", () => {
137
138
  test("default inserts once then already-existed without touching the row", async () => {
138
- const { handle, conn } = await openHandle();
139
- const first = await handle.upsert(
139
+ const first = await sharedHandle.upsert(
140
140
  posts,
141
141
  { id: "welcome" },
142
142
  { id: "welcome", title: "Hello", createdAt: 1 },
143
143
  );
144
144
  expect(first.status).toBe("upserted");
145
145
 
146
- const second = await handle.upsert(
146
+ const second = await sharedHandle.upsert(
147
147
  posts,
148
148
  { id: "welcome" },
149
149
  { id: "welcome", title: "Changed", createdAt: 2 },
150
150
  );
151
151
  expect(second.status).toBe("already-existed");
152
152
 
153
- const row = await handle.findById(posts, "welcome");
153
+ const row = await sharedHandle.findById(posts, "welcome");
154
154
  expect(row).toEqual({ id: "welcome", title: "Hello", createdAt: 1 });
155
- await conn.close();
156
155
  });
157
156
 
158
157
  test("onExisting update changes matched columns", async () => {
159
- const { handle, conn } = await openHandle();
160
- await handle.upsert(posts, { id: "n1" }, { id: "n1", title: "one", createdAt: 10 });
161
- const updated = await handle.upsert(
158
+ await sharedHandle.upsert(posts, { id: "n1" }, { id: "n1", title: "one", createdAt: 10 });
159
+ const updated = await sharedHandle.upsert(
162
160
  posts,
163
161
  { id: "n1" },
164
162
  { id: "n1", title: "two", createdAt: 20 },
165
163
  { onExisting: "update" },
166
164
  );
167
165
  expect(updated.status).toBe("changed");
168
- const row = await handle.findById(posts, "n1");
166
+ const row = await sharedHandle.findById(posts, "n1");
169
167
  expect(row).toEqual({ id: "n1", title: "two", createdAt: 20 });
170
- await conn.close();
171
168
  });
172
169
  });