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.
- package/package.json +1 -1
- package/site/content/docs/elements/ai.mdx +22 -1
- package/site/content/docs/elements/store.mdx +3 -1
- package/site/content/docs/elements/vault.mdx +19 -11
- package/site/content/docs/get-started/installation.mdx +7 -1
- package/site/content/docs/recipes/llama-cpp.mdx +10 -9
- package/site/content/docs/reference/cli.md +6 -2
- package/site/content/docs/reference/environment-variables.mdx +9 -9
- package/src/cli/ai-setup/ai-setup.test.ts +3 -1
- package/src/cli/ai-setup/apply.ts +61 -1
- package/src/cli/ask-seed.test.ts +4 -3
- package/src/cli/ask-seed.ts +5 -6
- package/src/cli/client-add.test.ts +2 -1
- package/src/cli/dev.test.ts +116 -0
- package/src/cli/dev.ts +107 -18
- package/src/cli/project-state.test.ts +50 -0
- package/src/cli/project-state.ts +123 -0
- package/src/cli/vault-cmd.test.ts +47 -18
- package/src/cli/vault-cmd.ts +2 -1
- package/src/compiler/extract.ts +12 -1
- package/src/console/server/console.test.ts +3 -1
- package/src/console/server/operator-db.test.ts +48 -17
- package/src/console/server/operator-db.ts +5 -1
- package/src/docker/derive.ts +24 -3
- package/src/docker/docker.test.ts +4 -2
- package/src/docker/index.ts +1 -0
- package/src/docker/recipes/index.ts +1 -0
- package/src/docker/recipes/llama-cpp.ts +20 -4
- package/src/drivers/ai-openai-compatible.ts +15 -3
- package/src/drivers/vault-builtin.test.ts +50 -42
- package/src/elements/ai/declare.ts +73 -3
- package/src/elements/ai/errors.test.ts +35 -0
- package/src/elements/ai/errors.ts +139 -0
- package/src/elements/ai/eval.ts +26 -1
- package/src/elements/ai/runtime.ts +140 -80
- package/src/elements/ai/tools.test.ts +1 -1
- package/src/elements/ai.test.ts +99 -2
- package/src/elements/ai.ts +11 -1
- package/src/elements/gate/config.ts +13 -3
- package/src/elements/gate/declare.ts +1 -1
- package/src/elements/gate/strategies.ts +2 -12
- package/src/elements/index.ts +2 -0
- package/src/elements/store/index-boot.test.ts +23 -6
- package/src/elements/store/resource.test.ts +38 -19
- package/src/elements/store/sql-session.test.ts +55 -58
- package/src/elements/vault/builtin-adapter.test.ts +115 -58
- package/src/elements/vault/builtin-adapter.ts +241 -47
- package/src/elements/vault/chaos-child.ts +424 -0
- package/src/elements/vault/chaos.test.ts +651 -0
- package/src/elements/vault/resilience.ts +6 -1
- package/src/elements/vault/security-checklist.test.ts +10 -8
- package/src/elements/vault/storage.ts +130 -27
- package/src/elements/vault/test-helpers.ts +368 -0
- package/src/elements/vault.ts +6 -0
- package/src/index.ts +2 -0
- package/src/kernel/app-auth.ts +98 -0
- package/src/kernel/app.ts +120 -78
- package/src/kernel/auto-registry.test.ts +52 -1
- package/src/kernel/boot.test.ts +4 -18
- package/src/kernel/element-registries.ts +19 -4
- package/src/kernel/errors.ts +3 -3
- package/src/kernel/fx.test.ts +25 -0
- package/src/kernel/fx.ts +4 -1
- package/src/manifest/types.ts +4 -0
- package/src/release/build-lib.ts +3 -0
- package/src/shared/lazy-src.ts +79 -0
- package/src/test/create-test-app.ts +16 -11
- 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 {
|
|
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
|
-
|
|
55
|
-
|
|
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 "./
|
|
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
|
}
|
package/src/elements/index.ts
CHANGED
|
@@ -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
|
|
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: {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
62
|
-
connection:
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
connection:
|
|
36
|
+
sharedHandle = createSqlStoreHandle("sql:app", {
|
|
37
|
+
connection: sharedConn,
|
|
29
38
|
classifications: new Map(),
|
|
30
39
|
routedRole: "primary",
|
|
31
40
|
domainDdl: "ensure",
|
|
32
41
|
});
|
|
33
|
-
|
|
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",
|
|
38
|
-
|
|
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
|
|
68
|
-
expect("findMany" in
|
|
69
|
-
expect("findFirst" in
|
|
70
|
-
expect("with" in
|
|
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
|
-
|
|
79
|
-
await
|
|
80
|
-
await
|
|
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
|
|
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
|
-
|
|
90
|
-
await
|
|
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
|
|
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
|
-
|
|
100
|
-
await
|
|
101
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
127
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
160
|
-
await
|
|
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
|
|
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
|
});
|