@jskit-ai/resource-runtime 0.2.12 → 0.2.14

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,152 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import knexLib from "knex";
4
+ import { defineTableResource } from "@jskit-ai/resource-core/shared/tableResource";
5
+ import { requireTestDatabase } from "@jskit-ai/database-runtime/server/testDatabases";
6
+ import { createTestResourceRuntime } from "../../src/test/index.js";
7
+
8
+ // Whole numbers stored in BIGINT columns that are not ids (a counter, a byte count, a read position), read through a
9
+ // handle on MariaDB/MySQL and on PostgreSQL alike. Both drivers answer BIGINT as text (mysql2 with the runtime's
10
+ // bigNumberStrings, pg for every int8); a field of type "integer" reads back as a number on every handle read, and an
11
+ // id field as text.
12
+
13
+ const WORKSPACE = Object.freeze({ workspace: { id: "1" } });
14
+ const RUNTIME_MYSQL = Object.freeze({ supportBigNumbers: true, bigNumberStrings: true, dateStrings: ["DATE"], timezone: "Z" });
15
+ // The largest whole number a JavaScript number holds exactly (2^53 - 1), and the next odd one past it.
16
+ const LARGEST_SAFE = "9007199254740991";
17
+ const PAST_SAFE = "9007199254740993";
18
+
19
+ // A PostgreSQL test on a run for another database, such as CI's MariaDB run, is left to the PostgreSQL run.
20
+ const POSTGRES_ELSEWHERE = process.env.DB_CLIENT && process.env.DB_CLIENT !== "pg"
21
+ ? `a PostgreSQL test, on a ${process.env.DB_CLIENT} run`
22
+ : false;
23
+
24
+ const counterResource = defineTableResource({
25
+ name: "counters",
26
+ tableName: "counters",
27
+ ownership: "workspace",
28
+ defaultSort: ["id"],
29
+ schema: {
30
+ id: { type: "id", primary: true },
31
+ workspaceId: { type: "id", nullable: true, search: true, storage: { column: "workspace_id" } },
32
+ total: { type: "integer", search: true },
33
+ readUpTo: { type: "integer", nullable: true, storage: { column: "read_up_to" } },
34
+ hiddenCount: { type: "integer", nullable: true, hidden: true, storage: { column: "hidden_count" } },
35
+ lastMessageId: { type: "id", nullable: true, storage: { column: "last_message_id" } }
36
+ }
37
+ });
38
+ const FIELDS = Object.freeze(["workspaceId", "total", "readUpTo", "hiddenCount", "lastMessageId"]);
39
+
40
+ /** A MariaDB/MySQL database of the test's own beside TEST_DB_NAME, dropped after it. */
41
+ async function mysqlKnex(context, suffix) {
42
+ const name = `${requireTestDatabase(process.env)}_${suffix}`;
43
+ const server = { host: process.env.DB_HOST, port: Number(process.env.DB_PORT) || 3306, user: process.env.DB_USER, password: process.env.DB_PASSWORD };
44
+ const admin = knexLib({ client: "mysql2", connection: server, pool: { min: 0, max: 1 } });
45
+ await admin.raw("DROP DATABASE IF EXISTS ??", [name]);
46
+ await admin.raw("CREATE DATABASE ??", [name]);
47
+ const knex = knexLib({ client: "mysql2", connection: { ...server, database: name, ...RUNTIME_MYSQL }, pool: { min: 0, max: 2 } });
48
+ context.after(async () => {
49
+ await knex.destroy();
50
+ await admin.raw("DROP DATABASE IF EXISTS ??", [name]);
51
+ await admin.destroy();
52
+ });
53
+ return knex;
54
+ }
55
+
56
+ /** A PostgreSQL schema of the test's own in TEST_DB_NAME, dropped after it. */
57
+ async function postgresKnex(context, suffix) {
58
+ const database = requireTestDatabase(process.env);
59
+ const schema = `${database}_${suffix}_${process.pid}`;
60
+ const connection = { host: process.env.DB_HOST, port: Number(process.env.DB_PORT) || 5432, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database };
61
+ const admin = knexLib({ client: "pg", connection, pool: { min: 0, max: 1 } });
62
+ await admin.raw("DROP SCHEMA IF EXISTS ?? CASCADE", [schema]);
63
+ await admin.raw("CREATE SCHEMA ??", [schema]);
64
+ const knex = knexLib({ client: "pg", connection, searchPath: [schema], pool: { min: 0, max: 2 } });
65
+ context.after(async () => {
66
+ await knex.destroy();
67
+ await admin.raw("DROP SCHEMA IF EXISTS ?? CASCADE", [schema]);
68
+ await admin.destroy();
69
+ });
70
+ return knex;
71
+ }
72
+
73
+ // The record fields a test compares: their values with their types.
74
+ const typed = (record) => Object.fromEntries(
75
+ ["id", ...FIELDS].filter((field) => Object.hasOwn(record, field)).map((field) => [field, record[field]])
76
+ );
77
+
78
+ /**
79
+ * Registers the stored-integer tests on one database.
80
+ * @param {"mysql" | "postgres"} dialect
81
+ */
82
+ function storedIntegerCases(dialect) {
83
+ const skip = dialect === "postgres" ? POSTGRES_ELSEWHERE : false;
84
+ const openKnex = dialect === "postgres" ? postgresKnex : mysqlKnex;
85
+
86
+ async function counters(context) {
87
+ const knex = await openKnex(context, "stored_ints");
88
+ await knex.schema.createTable("counters", (table) => {
89
+ table.bigIncrements("id");
90
+ table.bigInteger("workspace_id").nullable();
91
+ table.bigInteger("total").notNullable();
92
+ table.bigInteger("read_up_to").nullable();
93
+ table.bigInteger("hidden_count").nullable();
94
+ table.bigInteger("last_message_id").nullable();
95
+ });
96
+ const runtime = await createTestResourceRuntime({ knex });
97
+ const { counters: handle } = await runtime.own("counters.feature", [counterResource]);
98
+ return { knex, counters: handle };
99
+ }
100
+
101
+ test(`${dialect}: a non-id BIGINT field of type integer reads back as a number on every handle read; an id as text`, { skip }, async (context) => {
102
+ const { counters: handle } = await counters(context);
103
+ const options = { context: WORKSPACE };
104
+ const expected = { id: "1", workspaceId: "1", total: 5, readUpTo: Number(LARGEST_SAFE), hiddenCount: 3, lastMessageId: "11" };
105
+
106
+ const created = await handle.create({ workspaceId: "1", total: 5, readUpTo: Number(LARGEST_SAFE), hiddenCount: 3, lastMessageId: "11" }, options);
107
+ const { hiddenCount, ...visible } = expected;
108
+ assert.equal(hiddenCount, 3);
109
+ assert.deepEqual(typed(created), visible, "create answers the stored record without its hidden field");
110
+ assert.deepEqual(typed(await handle.get(created.id, { fields: FIELDS }, options)), expected);
111
+ assert.deepEqual((await handle.query({ fields: FIELDS, page: { size: 10 } }, options)).records.map(typed), [expected]);
112
+ assert.deepEqual((await handle.queryAll({ fields: FIELDS }, options)).map(typed), [expected]);
113
+ assert.deepEqual(typed(await handle.patch(created.id, { total: 6 }, options)), { ...visible, total: 6 });
114
+ assert.deepEqual(
115
+ (await handle.patchMany([{ id: created.id, total: 7 }], { ...options, returning: "records" })).map(typed),
116
+ [{ ...visible, total: 7 }]
117
+ );
118
+ assert.deepEqual(
119
+ (await handle.createMany([{ workspaceId: "1", total: 8 }], { ...options, returning: "records" })).map(typed),
120
+ [{ id: "2", workspaceId: "1", total: 8, readUpTo: null, lastMessageId: null }]
121
+ );
122
+ const locked = await handle.withTransaction(options, async (txOptions) => ({
123
+ lockMany: await handle.lockMany([created.id], { ...txOptions, fields: FIELDS }),
124
+ claim: await handle.claim({ filters: { total: 7 }, take: 1 }, { ...txOptions, fields: FIELDS })
125
+ }));
126
+ assert.deepEqual(locked.lockMany.map(typed), [{ ...expected, total: 7 }]);
127
+ assert.deepEqual(locked.claim.map(typed), [{ ...expected, total: 7 }]);
128
+ const totals = await handle.aggregate({
129
+ measures: { total: { sum: "total" }, highest: { max: "readUpTo" }, lowest: { min: "total" }, rows: { count: "*" } }
130
+ }, options);
131
+ assert.deepEqual(totals.map((row) => ({ ...row })), [{ total: 15, highest: Number(LARGEST_SAFE), lowest: 7, rows: 2 }]);
132
+ });
133
+
134
+ test(`${dialect}: past 2^53 a stored integer keeps its digits as text, never a rounded number; scopedQuery rows are the driver's text`, { skip }, async (context) => {
135
+ const { knex, counters: handle } = await counters(context);
136
+ const options = { context: WORKSPACE };
137
+ await knex("counters").insert({ workspace_id: 1, total: PAST_SAFE, read_up_to: LARGEST_SAFE, hidden_count: PAST_SAFE });
138
+
139
+ const [record] = await handle.queryAll({ fields: FIELDS }, options);
140
+ assert.deepEqual(typed(record), { id: "1", workspaceId: "1", total: PAST_SAFE, readUpTo: Number(LARGEST_SAFE), hiddenCount: PAST_SAFE, lastMessageId: null });
141
+
142
+ // A report's own builder answers what the driver read: BIGINT as text on both drivers, ids and counters alike,
143
+ // and a COUNT too, which both databases answer as a BIGINT.
144
+ const { query } = await handle.scopedQuery({ filters: {} }, options);
145
+ const rows = await query.select("id", "total", "read_up_to");
146
+ assert.deepEqual(rows.map((row) => ({ ...row })), [{ id: "1", total: PAST_SAFE, read_up_to: LARGEST_SAFE }]);
147
+ const { query: counting } = await handle.scopedQuery({ filters: {} }, options);
148
+ assert.deepEqual((await counting.count({ rows: "*" })).map((row) => ({ ...row })), [{ rows: "1" }]);
149
+ });
150
+ }
151
+
152
+ export { storedIntegerCases };
@@ -4,14 +4,10 @@ import knexFactory from "knex";
4
4
  import { defineTableResource } from "@jskit-ai/resource-core/shared/tableResource";
5
5
  import { defineFeature } from "@jskit-ai/kernel/server/features";
6
6
  import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
7
- import {
8
- TestDatabaseMissingError,
9
- createTestResourceRuntime,
10
- expectEveryTableOwned,
11
- expectFlatQueries,
12
- expectScoped,
13
- startTestFeatures
14
- } from "../src/test/index.js";
7
+ import { createPackageTestKnex, createTestResourceRuntime, expectEveryTableOwned, expectFlatQueries, expectScoped, startTestFeatures, TestDatabaseMissingError } from "../src/test/index.js";
8
+
9
+ // SQLite in memory, or a PostgreSQL schema of its own on a DB_CLIENT=pg run (see createPackageTestKnex).
10
+ const testKnex = (t) => createPackageTestKnex(t, { environment: process.env });
15
11
 
16
12
  const environment = Object.freeze({ TEST_DB_NAME: "catalogue_test" });
17
13
 
@@ -70,8 +66,7 @@ test("expectFlatQueries() passes a path that reads its rows at once and fails on
70
66
  });
71
67
 
72
68
  async function createShelfFixture(t) {
73
- const knex = knexFactory({ client: "better-sqlite3", connection: { filename: ":memory:" }, useNullAsDefault: true });
74
- t.after(() => knex.destroy());
69
+ const knex = await testKnex(t);
75
70
  await knex.schema.createTable("books", (table) => {
76
71
  table.increments("id");
77
72
  table.integer("workspace_id");
@@ -147,8 +142,7 @@ test("expectScoped() passes a path that keeps to its workspace and fails one tha
147
142
  });
148
143
 
149
144
  test("expectScoped() fails a report that joins another owner's table by name, though no row of it is in the answer", async (t) => {
150
- const knex = knexFactory({ client: "better-sqlite3", connection: { filename: ":memory:" }, useNullAsDefault: true });
151
- t.after(() => knex.destroy());
145
+ const knex = await testKnex(t);
152
146
  await knex.schema.createTable("visits", (table) => { table.increments("id"); table.integer("workspace_id"); table.string("person"); });
153
147
  await knex.schema.createTable("people", (table) => { table.increments("id"); table.integer("workspace_id"); table.string("name"); table.string("business"); });
154
148
  const owned = (name, schema) => defineTableResource({
@@ -188,8 +182,7 @@ test("expectScoped() fails a report that joins another owner's table by name, th
188
182
  });
189
183
 
190
184
  test("expectScoped() compares the other owner's stored rows, so a changed hidden column or one outside the schema fails", async (t) => {
191
- const knex = knexFactory({ client: "better-sqlite3", connection: { filename: ":memory:" }, useNullAsDefault: true });
192
- t.after(() => knex.destroy());
185
+ const knex = await testKnex(t);
193
186
  await knex.schema.createTable("keys", (table) => {
194
187
  table.increments("id");
195
188
  table.integer("workspace_id");
@@ -248,8 +241,7 @@ test("expectEveryTableOwned() names each table no feature owns", async (t) => {
248
241
  });
249
242
 
250
243
  test("startTestFeatures() starts features in the order their requires give, on tables it creates, and boots them", async (t) => {
251
- const knex = knexFactory({ client: "better-sqlite3", connection: { filename: ":memory:" }, useNullAsDefault: true });
252
- t.after(() => knex.destroy());
244
+ const knex = await testKnex(t);
253
245
  const shelves = defineTableResource({
254
246
  name: "shelves",
255
247
  tableName: "shelves",