@jskit-ai/resource-runtime 0.0.0-stage → 0.2.13

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 (43) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/locales/en.json +16 -0
  3. package/package.json +68 -4
  4. package/src/server/ResourceRuntimeProvider.js +30 -0
  5. package/src/server/changeHorizon.js +26 -0
  6. package/src/server/contexts.js +192 -0
  7. package/src/server/errors.js +137 -0
  8. package/src/server/foreignKeyCheck.js +58 -0
  9. package/src/server/handleGuards.js +233 -0
  10. package/src/server/handleHidden.js +410 -0
  11. package/src/server/handleLocks.js +135 -0
  12. package/src/server/handleRecords.js +474 -0
  13. package/src/server/index.js +18 -0
  14. package/src/server/jsonRestValues.js +49 -0
  15. package/src/server/lockOrCreate.js +92 -0
  16. package/src/server/rawExpression.js +50 -0
  17. package/src/server/resourceHandle.js +737 -0
  18. package/src/server/resourceHost.js +687 -0
  19. package/src/server/resourceRuntime.js +300 -0
  20. package/src/server/standaloneResources.js +49 -0
  21. package/src/server/types.js +49 -0
  22. package/src/test/expectFlatQueries.js +117 -0
  23. package/src/test/expectScoped.js +111 -0
  24. package/src/test/index.js +123 -0
  25. package/test/callSettings.test.js +100 -0
  26. package/test/changes.postgres.test.js +141 -0
  27. package/test/entrypoints.boundary.test.js +895 -0
  28. package/test/foreignKeys.test.js +94 -0
  29. package/test/hiddenFields.test.js +244 -0
  30. package/test/i18nCatalog.test.js +18 -0
  31. package/test/locks.postgres.test.js +345 -0
  32. package/test/lostInsertRace.test.js +65 -0
  33. package/test/manyRowWrites.mysql.test.js +154 -0
  34. package/test/queryFields.test.js +69 -0
  35. package/test/requestCallOptions.test.js +155 -0
  36. package/test/resourceRuntime.test.js +2098 -0
  37. package/test/storedIntegers.mysql.test.js +4 -0
  38. package/test/storedIntegers.postgres.test.js +5 -0
  39. package/test/support/storedIntegerCases.js +152 -0
  40. package/test/testHelpers.test.js +290 -0
  41. package/test/workspaceScopeValue.test.js +49 -0
  42. package/test/writeValues.mysql.test.js +111 -0
  43. package/README.md +0 -3
@@ -0,0 +1,94 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import knexLib from "knex";
4
+ import { silentLogger } from "@jskit-ai/kernel/server/runtime/logger";
5
+ import { defineTableResource } from "@jskit-ai/resource-core/shared/tableResource";
6
+ import { checkForeignKeys } from "../src/server/index.js";
7
+ import { createResourceHost } from "../src/server/resourceHost.js";
8
+ import { createResourceRuntime } from "../src/server/resourceRuntime.js";
9
+
10
+ const owners = defineTableResource({
11
+ name: "owners",
12
+ tableName: "owners",
13
+ ownership: "public",
14
+ schema: { name: { type: "string", required: true } }
15
+ });
16
+
17
+ function petsResource(foreignKeys) {
18
+ return defineTableResource({
19
+ name: "pets",
20
+ tableName: "pets",
21
+ ownership: "public",
22
+ schema: {
23
+ name: { type: "string", required: true },
24
+ ownerId: { type: "id", nullable: true, belongsTo: "owners", as: "owner" },
25
+ vetId: { type: "id", nullable: true, storage: { column: "clinic_vet" } }
26
+ },
27
+ ...(foreignKeys ? { foreignKeys } : {})
28
+ });
29
+ }
30
+
31
+ async function petsDatabase(t) {
32
+ const knex = knexLib({ client: "better-sqlite3", connection: { filename: ":memory:" }, useNullAsDefault: true });
33
+ t.after(() => knex.destroy());
34
+ await knex.schema.createTable("owners", (table) => {
35
+ table.increments("id");
36
+ table.string("name").notNullable();
37
+ });
38
+ await knex.schema.createTable("vets", (table) => table.increments("id"));
39
+ await knex.schema.createTable("pets", (table) => {
40
+ table.increments("id");
41
+ table.string("name").notNullable();
42
+ table.integer("owner_id").references("id").inTable("owners");
43
+ table.integer("clinic_vet");
44
+ });
45
+ return knex;
46
+ }
47
+
48
+ test("a declared foreignKeys reaches the json-rest-api resource, and a table without one declares none", async (t) => {
49
+ const knex = await petsDatabase(t);
50
+ const api = await createResourceHost({ knex, logger: silentLogger });
51
+ const declared = [{ columns: ["ownerId"], referencedTableName: "owners", deleteRule: "SET NULL" }];
52
+ const runtime = createResourceRuntime({ api });
53
+ const { pets } = await runtime.own("app.pets", [owners, petsResource(declared)]);
54
+ const context = runtime.declareSystemScope("app.pets", "The test seeds every owner's pets.")("seed a pet");
55
+
56
+ assert.deepEqual(api.resources.pets.vars.schemaInfo.foreignKeys, declared);
57
+ assert.deepEqual(api.resources.owners.vars.schemaInfo.foreignKeys, []);
58
+ const [ownerId] = await knex("owners").insert({ name: "Ada" });
59
+ const created = await pets.create({ name: "Rex", ownerId: String(ownerId) }, { context });
60
+ assert.equal(created.ownerId, String(ownerId));
61
+ assert.equal((await pets.get(created.id, {}, { context })).ownerId, String(ownerId));
62
+ });
63
+
64
+ test("checkForeignKeys finds a declared key the database lacks, and reports a real key nothing declares", async (t) => {
65
+ const knex = await petsDatabase(t);
66
+
67
+ const exact = await checkForeignKeys({
68
+ knex,
69
+ resources: [owners, petsResource([{ columns: ["ownerId"], referencedTableName: "owners" }])],
70
+ tables: ["owners", "pets"]
71
+ });
72
+ assert.deepEqual(exact, { missing: [], undeclared: [] });
73
+
74
+ const wrong = await checkForeignKeys({
75
+ knex,
76
+ resources: [petsResource([
77
+ { columns: ["ownerId"], referencedTableName: "owners", referencedColumns: ["id"] },
78
+ { name: "pets_vet", columns: ["vetId"], referencedTableName: "vets" }
79
+ ])],
80
+ tables: ["pets"]
81
+ });
82
+ assert.deepEqual(wrong.missing.map(({ resource, tableName, columns, referencedTableName, referencedColumns, declared }) =>
83
+ ({ resource, tableName, columns, referencedTableName, referencedColumns, name: declared.name })), [
84
+ { resource: "pets", tableName: "pets", columns: ["clinic_vet"], referencedTableName: "vets", referencedColumns: ["id"], name: "pets_vet" }
85
+ ]);
86
+ assert.deepEqual(wrong.undeclared, []);
87
+
88
+ const undeclared = await checkForeignKeys({ knex, resources: [owners, petsResource()], tables: ["pets"] });
89
+ assert.deepEqual(undeclared, {
90
+ missing: [],
91
+ undeclared: [{ name: "", tableName: "pets", columns: ["owner_id"], referencedTableName: "owners", referencedColumns: ["id"] }]
92
+ });
93
+ assert.deepEqual((await checkForeignKeys({ knex, resources: [petsResource()], tables: [] })).undeclared, []);
94
+ });
@@ -0,0 +1,244 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { defineTableResource } from "@jskit-ai/resource-core/shared/tableResource";
4
+ import { countQueries } from "@jskit-ai/database-runtime/server/testDatabases";
5
+ import { withoutHiddenFields } from "../src/server/index.js";
6
+ import { createPackageTestKnex, createTestResourceRuntime } from "../src/test/index.js";
7
+
8
+ // SQLite in memory, or a PostgreSQL schema of its own on a DB_CLIENT=pg run (see createPackageTestKnex).
9
+ const testKnex = (t) => createPackageTestKnex(t, { environment: process.env });
10
+
11
+ const WORKSPACE_A = Object.freeze({ workspace: { id: "1" } });
12
+
13
+ // Cards with a hidden owner, a hidden bearer token, a hidden flag, a hidden count and a hidden time: two in workspace
14
+ // 1 and one in workspace 2.
15
+ async function createCardFixture(t) {
16
+ const knex = await testKnex(t);
17
+ await knex.schema.createTable("cards", (table) => {
18
+ table.increments("id");
19
+ table.integer("workspace_id").notNullable();
20
+ table.string("title").notNullable();
21
+ table.string("public_token");
22
+ table.boolean("sent");
23
+ table.integer("views");
24
+ table.dateTime("issued_at");
25
+ });
26
+ await knex("cards").insert([
27
+ { workspace_id: 1, title: "Rex", public_token: "t1", sent: 1, views: 3 },
28
+ { workspace_id: 1, title: "Bo", public_token: "t2", sent: 0, views: null },
29
+ { workspace_id: 2, title: "Kit", public_token: "t3", sent: 1, views: 1 }
30
+ ]);
31
+ const resource = defineTableResource({
32
+ name: "cards",
33
+ tableName: "cards",
34
+ ownership: "workspace",
35
+ schema: {
36
+ workspaceId: { type: "id", required: true, hidden: true, storage: { column: "workspace_id" } },
37
+ title: { type: "string", required: true, search: true },
38
+ publicToken: { type: "string", nullable: true, hidden: true, storage: { column: "public_token" } },
39
+ sent: { type: "boolean", nullable: true, hidden: true },
40
+ views: { type: "integer", nullable: true, hidden: true },
41
+ issuedAt: { type: "dateTime", nullable: true, hidden: true, storage: { column: "issued_at" } }
42
+ }
43
+ });
44
+ const runtime = await createTestResourceRuntime({ knex });
45
+ const { cards } = await runtime.own("cards.feature", [resource]);
46
+ const system = runtime.declareSystemScope("cards.feature", "send each workspace its cards")("send the cards");
47
+ // The tables each SELECT reads, in order.
48
+ const selects = async (work) => (await countQueries(knex, work)).queries
49
+ .filter((sql) => /^select/iu.test(sql))
50
+ .map((sql) => [...sql.matchAll(/from [`"](\w+)[`"]/gu)].map(([, table]) => table)[0]);
51
+ return { knex, cards, resource, system, selects, options: { context: WORKSPACE_A } };
52
+ }
53
+
54
+ test("a handle's plain read answers the hidden fields its fields names, in one more statement, and no read without them does", async (t) => {
55
+ const { cards, system, selects, options } = await createCardFixture(t);
56
+ let card;
57
+ const plainRead = await selects(async () => {
58
+ card = await cards.get("1", {}, options);
59
+ });
60
+ assert.deepEqual(card, { id: "1", title: "Rex" });
61
+ assert.deepEqual(await selects(async () => {
62
+ card = await cards.get("1", { fields: ["title", "publicToken", "workspaceId"] }, options);
63
+ }), [...plainRead, "cards"]);
64
+ assert.deepEqual(card, { id: "1", title: "Rex", publicToken: "t1", workspaceId: "1" });
65
+ assert.equal(await cards.get("3", { fields: ["publicToken"] }, { ...options, missing: "skip" }), null);
66
+
67
+ const { records } = await cards.query({ fields: ["title", "sent", "views"], page: { size: 10 } }, options);
68
+ assert.deepEqual(records, [{ id: "1", title: "Rex", sent: true, views: 3 }, { id: "2", title: "Bo", sent: false, views: null }]);
69
+ assert.deepEqual(await cards.queryAll({ filters: { title: "Bo" }, fields: ["publicToken"] }, options), [{ id: "2", publicToken: "t2" }]);
70
+
71
+ // A system context reads every workspace's rows, and learns each one's owner.
72
+ const owners = await cards.queryAll({ fields: ["workspaceId"] }, { context: system });
73
+ assert.deepEqual(owners, [{ id: "1", workspaceId: "1" }, { id: "2", workspaceId: "1" }, { id: "3", workspaceId: "2" }]);
74
+
75
+ // The locks read them after the locking read, in the same transaction.
76
+ await cards.withTransaction(options, async (tx) => {
77
+ assert.deepEqual(await cards.lock("1", { ...tx, fields: ["publicToken"] }), { id: "1", publicToken: "t1" });
78
+ assert.deepEqual(await cards.lockMany(["1", "2"], { ...tx, fields: ["sent"] }), [{ id: "1", sent: true }, { id: "2", sent: false }]);
79
+ assert.deepEqual(await cards.lockWhere({ title: "Rex" }, { ...tx, fields: ["views"] }), [{ id: "1", views: 3 }]);
80
+ assert.deepEqual(await cards.claim({ filters: { title: "Bo" } }, { ...tx, fields: ["title", "publicToken"] }),
81
+ [{ id: "2", title: "Bo", publicToken: "t2" }]);
82
+ });
83
+ });
84
+
85
+ test("a JSON:API read never answers a hidden field, and a hidden field a read cannot decode is refused before it runs", async (t) => {
86
+ const { cards, selects, options } = await createCardFixture(t);
87
+ const document = await cards.query({ fields: ["title", "publicToken"], page: { size: 10 } }, { ...options, format: "jsonapi" });
88
+ assert.deepEqual(document.data.map(({ attributes }) => attributes), [{ title: "Rex" }, { title: "Bo" }]);
89
+ const one = await cards.get("1", { fields: ["title", "publicToken"] }, { ...options, format: "jsonapi" });
90
+ assert.deepEqual(one.data.attributes, { title: "Rex" });
91
+
92
+ assert.deepEqual(await selects(() => assert.rejects(cards.queryAll({ fields: ["issuedAt"] }, options),
93
+ /cards: fields names the hidden field "issuedAt", but a read answers a hidden field only when it is a stored id/u)), []);
94
+ });
95
+
96
+ test("withoutHiddenFields refuses another feature's read naming a hidden field, unless it runs in a system context", async (t) => {
97
+ const { cards, resource, system, options } = await createCardFixture(t);
98
+ const exposed = withoutHiddenFields(cards, resource);
99
+ await assert.rejects(exposed.get("1", { fields: ["title", "publicToken"] }, options),
100
+ /cards\.get\(\) fields names the hidden field "publicToken", which only the table's owner, or a system context, reads/u);
101
+ await assert.rejects(exposed.queryAll({ fields: ["workspaceId", "sent"] }, options), /hidden field "workspaceId", "sent"/u);
102
+ await assert.rejects(exposed.withTransaction(options, (tx) => exposed.lockMany(["1"], { ...tx, fields: ["publicToken"] })),
103
+ /cards\.lockMany\(\) fields names the hidden field "publicToken"/u);
104
+ assert.deepEqual(await exposed.get("1", { fields: ["title"] }, options), { id: "1", title: "Rex" });
105
+ assert.deepEqual(await exposed.queryAll({ filters: { title: "Kit" }, fields: ["workspaceId"] }, { context: system }),
106
+ [{ id: "3", workspaceId: "2" }]);
107
+ assert.deepEqual(await exposed.queryAll({ filters: { title: "Kit" }, fields: ["publicToken"] }, { context: system.forWorkspace("2") }),
108
+ [{ id: "3", publicToken: "t3" }]);
109
+ // Everything else is the repository's own, and a resource without hidden fields keeps its repository.
110
+ assert.equal(exposed.column("title"), cards.column("title"));
111
+ assert.equal(exposed.name, "cards");
112
+ assert.equal(Object.isFrozen(exposed), true);
113
+ const plain = defineTableResource({ name: "notes", tableName: "notes", ownership: "public", schema: { body: { type: "string" } } });
114
+ assert.equal(withoutHiddenFields(cards, plain), cards);
115
+ });
116
+
117
+ test("withoutHiddenFields refuses another feature's scopedQuery, and any builder a method answers but a joinSource()'s, which carries no hidden column", async (t) => {
118
+ const { knex, cards, resource, system, options } = await createCardFixture(t);
119
+ // Methods an owner's decorateRepository adds with the handle it receives, answering its scoped rows as scopedQuery()
120
+ // gives them, every column with them: wrapped, and at once.
121
+ async function cardSource(alias, callOptions) {
122
+ const { query } = await cards.scopedQuery({ alias }, callOptions);
123
+ return { query, column: (field) => cards.column(field, alias) };
124
+ }
125
+ const everyColumn = () => knex("cards");
126
+ const exposed = withoutHiddenFields(Object.freeze({ ...cards, cardSource, everyColumn }), resource);
127
+
128
+ // Another feature's own builder would select the token: refused before it reads anything.
129
+ await assert.rejects(
130
+ (async () => (await exposed.scopedQuery({}, options)).query.select(exposed.column("publicToken")))(),
131
+ /cards\.scopedQuery\(\) answers every column, its hidden fields too, which only the table's owner, or a system context, reads: read the rows through the repository's reads, or join its joinSource\(\{ alias \}, options\)/u
132
+ );
133
+ await assert.rejects(exposed.withTransaction(options, (tx) => exposed.scopedQuery({ alias: "c" }, tx)), /cards\.scopedQuery\(\)/u);
134
+ await assert.rejects(exposed.cardSource("c", options),
135
+ /cards\.cardSource\(\) answers a query builder, which can select every column, the hidden fields too, which only the table's owner, or a system context, reads: answer the table's joinSource\(\{ alias \}, options\)/u);
136
+ assert.throws(() => exposed.everyColumn(), /cards\.everyColumn\(\) answers a query builder/u);
137
+
138
+ // A system context, and one it derived, still builds on them.
139
+ const { query: everyWorkspace } = await exposed.scopedQuery({}, { context: system });
140
+ assert.deepEqual(Number((await everyWorkspace.count({ cards: exposed.column("id") }))[0].cards), 3);
141
+ const { query: workspaceTwo } = await exposed.scopedQuery({}, { context: system.forWorkspace("2") });
142
+ assert.deepEqual(await workspaceTwo.select({ title: exposed.column("title") }), [{ title: "Kit" }]);
143
+ const everyCard = await exposed.cardSource("c", { context: system });
144
+ assert.deepEqual(await everyCard.query.select({ token: everyCard.column("publicToken") }).orderBy(everyCard.column("id")),
145
+ [{ token: "t1" }, { token: "t2" }, { token: "t3" }]);
146
+
147
+ // The cross-owner report pattern: the table's joinSource() answers the caller's scoped rows, which the report
148
+ // selects from, with the columns a read answers and no other.
149
+ const source = await exposed.joinSource({ alias: "c" }, options);
150
+ const rows = await source.query.select({ title: source.column("title") }).orderBy(source.column("id"));
151
+ assert.deepEqual(rows, [{ title: "Rex" }, { title: "Bo" }]);
152
+ const { query: rex } = await exposed.joinSource({ alias: "c", filters: { title: "Rex" } }, options);
153
+ assert.deepEqual((await rex).map((row) => Object.keys(row)), [["id", "title"]]);
154
+ assert.throws(() => source.column("publicToken"), /cards\.joinSource\(\) carries no hidden field, so it has no column for "publicToken"/u);
155
+ const again = await exposed.joinSource({ alias: "c" }, options);
156
+ await assert.rejects(async () => again.query.select(cards.column("publicToken", "c")), /public_token/u);
157
+ await assert.rejects(exposed.joinSource({}, options), /cards\.joinSource\(\) needs the alias the report joins the cards under/u);
158
+ });
159
+
160
+ test("withoutHiddenFields answers raw(), column(), joinSource() and an owner's Knex values as they are, so another feature builds on them in a scoped query and a subquery", async (t) => {
161
+ const { cards, resource, options } = await createCardFixture(t);
162
+ // An owner's method answering a Knex value at once: an expression over its own column.
163
+ const lowerTitle = (alias) => cards.raw("LOWER(??)", [cards.column("title", alias)]);
164
+ const exposed = withoutHiddenFields(Object.freeze({ ...cards, lowerTitle }), resource);
165
+
166
+ assert.equal(exposed.column("title", "c"), cards.column("title", "c"));
167
+ const shout = exposed.raw("UPPER(??)", [exposed.column("title", "c")]);
168
+ assert.equal(shout.then, undefined);
169
+ assert.equal(typeof exposed.lowerTitle("i").toSQL, "function");
170
+
171
+ // In the owner's scoped rows, as a select.
172
+ const source = await exposed.joinSource({ alias: "c" }, options);
173
+ assert.deepEqual(await source.query.select({ shout }).orderBy(source.column("id")), [{ shout: "REX" }, { shout: "BO" }]);
174
+
175
+ // In a subquery: the cards of workspace 1 whose lower-cased title is "bo".
176
+ const outer = await exposed.joinSource({ alias: "c" }, options);
177
+ const inner = await exposed.joinSource({ alias: "i" }, options);
178
+ const matching = inner.query.select(inner.column("id")).where(exposed.lowerTitle("i"), exposed.raw("?", ["bo"]));
179
+ assert.deepEqual(await outer.query.whereIn(outer.column("id"), matching).select({ title: outer.column("title") }), [{ title: "Bo" }]);
180
+ });
181
+
182
+ test("withoutHiddenFields refuses a hidden field named through any method, and strips the hidden fields from every answer of a read the owner adds, unless it runs in a system context", async (t) => {
183
+ const { cards, resource, system, options } = await createCardFixture(t);
184
+ // The owner's decorateRepository reads: one forwarding the caller's params, the handle's own under another name, one
185
+ // taking its options first, one answering a map, one in a sub-repository and one answering at once.
186
+ const owner = Object.freeze({
187
+ ...cards,
188
+ queryAllIncludingArchived: (params, callOptions) => cards.queryAll(params, callOptions),
189
+ everyCard: cards.queryAll,
190
+ cardsTitled: (callOptions, { title }) => cards.queryAll({ filters: { title }, fields: ["title", "publicToken"] }, callOptions),
191
+ async cardsByTitle(callOptions) {
192
+ const records = await cards.queryAll({ fields: ["title", "publicToken"] }, callOptions);
193
+ return new Map(records.map((record) => [record.title, record]));
194
+ },
195
+ archive: Object.freeze({ list: (callOptions) => cards.queryAll({ fields: ["title", "views"] }, callOptions) }),
196
+ tokenLength: (card) => String(card.publicToken).length
197
+ });
198
+ const exposed = withoutHiddenFields(owner, resource);
199
+
200
+ // Another feature's calls: a hidden field its fields names is refused before the method runs, whatever the method's
201
+ // name, as a list or a sparse fieldset, so a method forwarding the caller's fields never answers it as undefined.
202
+ await assert.rejects(exposed.queryAllIncludingArchived({ filters: { title: "Rex" }, fields: ["title", "publicToken", "workspaceId"] }, options),
203
+ /cards\.queryAllIncludingArchived\(\) fields names the hidden field "publicToken", "workspaceId", which only the table's owner, or a system context, reads/u);
204
+ await assert.rejects(exposed.everyCard({ filters: { title: "Bo" }, fields: { cards: "title, sent" } }, options),
205
+ /cards\.everyCard\(\) fields names the hidden field "sent"/u);
206
+ assert.deepEqual(await exposed.queryAllIncludingArchived({ filters: { title: "Rex" }, fields: ["title"] }, options), [{ id: "1", title: "Rex" }]);
207
+ // No hidden field the owner's own reads name leaves, whatever the method's name or depth.
208
+ assert.deepEqual(await exposed.cardsTitled(options, { title: "Rex" }), [{ id: "1", title: "Rex" }]);
209
+ assert.deepEqual([...(await exposed.cardsByTitle(options)).values()], [{ id: "1", title: "Rex" }, { id: "2", title: "Bo" }]);
210
+ assert.deepEqual(await exposed.archive.list(options), [{ id: "1", title: "Rex" }, { id: "2", title: "Bo" }]);
211
+ // A method that answers at once still does, and withTransaction answers the caller's own work as it is.
212
+ assert.equal(exposed.tokenLength({ publicToken: "abc" }), 3);
213
+ assert.deepEqual(await exposed.withTransaction(options, async () => ({ publicToken: "the caller's own" })), { publicToken: "the caller's own" });
214
+
215
+ // The owner, with its own repository, and a system context, or one it derived, still read them when named.
216
+ assert.deepEqual(await owner.queryAllIncludingArchived({ filters: { title: "Rex" }, fields: ["publicToken"] }, options),
217
+ [{ id: "1", publicToken: "t1" }]);
218
+ assert.deepEqual(await exposed.queryAllIncludingArchived({ filters: { title: "Kit" }, fields: ["publicToken", "workspaceId"] }, { context: system }),
219
+ [{ id: "3", publicToken: "t3", workspaceId: "2" }]);
220
+ assert.deepEqual(await exposed.archive.list({ context: system.forWorkspace("2") }), [{ id: "3", title: "Kit", views: 1 }]);
221
+ // The system context counts in whichever record carries it, not only the last.
222
+ assert.deepEqual(await exposed.cardsTitled({ context: system.forWorkspace("2") }, { title: "Kit" }), [{ id: "3", title: "Kit", publicToken: "t3" }]);
223
+ });
224
+
225
+ test("withoutHiddenFields refuses a field one owned table hides and another shows, since it strips hidden fields by name", () => {
226
+ const cards = defineTableResource({
227
+ name: "cards",
228
+ tableName: "cards",
229
+ ownership: "public",
230
+ schema: { title: { type: "string" }, publicToken: { type: "string", hidden: true } }
231
+ });
232
+ const links = (hidden) => defineTableResource({
233
+ name: "links",
234
+ tableName: "links",
235
+ ownership: "public",
236
+ schema: { publicToken: { type: "string", ...(hidden ? { hidden } : {}) } }
237
+ });
238
+ const repository = Object.freeze({ get: async () => ({ id: "1", publicToken: "t1" }) });
239
+ assert.throws(() => withoutHiddenFields(repository, [cards, links(false)]),
240
+ /links\.publicToken is a visible field, but cards, which the same feature owns, hides a field of that name; its repository strips hidden fields by name from every answer/u);
241
+ assert.throws(() => withoutHiddenFields(repository, [links(false), cards]), /links\.publicToken is a visible field, but cards/u);
242
+ // Hidden in both, it is one hidden field.
243
+ assert.equal(typeof withoutHiddenFields(repository, [cards, links(true)]).get, "function");
244
+ });
@@ -0,0 +1,18 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import test from "node:test";
4
+ import { fileURLToPath } from "node:url";
5
+ import { evaluateI18nCatalogContract } from "../../../tooling/test-support/i18nCatalogContract.mjs";
6
+
7
+ const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
8
+ const REPO_ROOT = path.resolve(TEST_DIRECTORY, "..", "..", "..");
9
+
10
+ test("resource-runtime English catalog matches its source", () => {
11
+ const { problems } = evaluateI18nCatalogContract({
12
+ repoRoot: REPO_ROOT,
13
+ packageDir: path.join(REPO_ROOT, "packages", "resource-runtime"),
14
+ // json-rest-api throws these; their English is fixed by the library.
15
+ externalCodes: ["REST_API_VERSION_CONFLICT", "REST_API_PRECONDITION_FAILED"]
16
+ });
17
+ assert.deepEqual(problems, []);
18
+ });