@substrat-run/contract-tests 0.144.0 → 0.145.0
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/dist/bind-module.d.ts +22 -1
- package/dist/bind-module.d.ts.map +1 -1
- package/dist/bind-module.js +32 -1
- package/dist/bind-module.js.map +1 -1
- package/dist/binding-invite-suite.d.ts.map +1 -1
- package/dist/binding-invite-suite.js +53 -0
- package/dist/binding-invite-suite.js.map +1 -1
- package/dist/derived-module.d.ts +72 -1
- package/dist/derived-module.d.ts.map +1 -1
- package/dist/derived-module.js +31 -3
- package/dist/derived-module.js.map +1 -1
- package/dist/derived-suite.d.ts.map +1 -1
- package/dist/derived-suite.js +91 -0
- package/dist/derived-suite.js.map +1 -1
- package/dist/entity-state-module.d.ts +16 -0
- package/dist/entity-state-module.d.ts.map +1 -1
- package/dist/entity-trash-module.d.ts +80 -0
- package/dist/entity-trash-module.d.ts.map +1 -1
- package/dist/entity-trash-module.js +73 -0
- package/dist/entity-trash-module.js.map +1 -1
- package/dist/entity-trash-suite.d.ts.map +1 -1
- package/dist/entity-trash-suite.js +94 -2
- package/dist/entity-trash-suite.js.map +1 -1
- package/dist/erasure-module.d.ts +8 -0
- package/dist/erasure-module.d.ts.map +1 -1
- package/dist/findings-suite.d.ts.map +1 -1
- package/dist/findings-suite.js +55 -1
- package/dist/findings-suite.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/modules.d.ts +248 -0
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +6 -1
- package/dist/modules.js.map +1 -1
- package/dist/scope-host-suite.d.ts.map +1 -1
- package/dist/scope-host-suite.js +501 -0
- package/dist/scope-host-suite.js.map +1 -1
- package/dist/stamps-module.d.ts +462 -0
- package/dist/stamps-module.d.ts.map +1 -0
- package/dist/stamps-module.js +242 -0
- package/dist/stamps-module.js.map +1 -0
- package/dist/stamps-suite.d.ts +3 -0
- package/dist/stamps-suite.d.ts.map +1 -0
- package/dist/stamps-suite.js +170 -0
- package/dist/stamps-suite.js.map +1 -0
- package/dist/vertical-events-suite.d.ts +16 -0
- package/dist/vertical-events-suite.d.ts.map +1 -1
- package/dist/vertical-events-suite.js +30 -3
- package/dist/vertical-events-suite.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixture behind `updatedStampsContractSuite` (#2166): an entity that declares all four stamps,
|
|
3
|
+
* written by derived and authored handlers alike, and one that declares only `createdAt`.
|
|
4
|
+
*
|
|
5
|
+
* - `sdoc` — every stamp, and archivable: a derived create, get, update and archive, plus authored
|
|
6
|
+
* writes that stamp the updated pair right or wrong on purpose (`stamp`), an authored delete, a
|
|
7
|
+
* write inside a `ctx.atomic` that is rolled back, and a consumer that writes the row.
|
|
8
|
+
* - `splain` — the created stamp only: an authored write that stamps nothing is never checked.
|
|
9
|
+
*
|
|
10
|
+
* Inert for every other suite: nothing else invokes a `stamps/*` operation or reads its tables.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { defineEntities, defineOperations, entityStateOutput, manifestEntities, manifestOperations, moduleManifest, permissionKey, } from '@substrat-run/contracts';
|
|
14
|
+
import { assertAllowed, operationsFor, readHistory } from '@substrat-run/kernel';
|
|
15
|
+
export const STAMPS_MODULE_ID = '@test/stamps';
|
|
16
|
+
/** A time no call is ever made at. */
|
|
17
|
+
export const STALE_TIME = '2000-01-01T00:00:00.000Z';
|
|
18
|
+
export const stampsEntities = defineEntities({
|
|
19
|
+
sdoc: {
|
|
20
|
+
table: 'stamped_docs',
|
|
21
|
+
fields: z.object({
|
|
22
|
+
id: z.string(),
|
|
23
|
+
title: z.string(),
|
|
24
|
+
created_at: z.string(),
|
|
25
|
+
created_by: z.string(),
|
|
26
|
+
updated_at: z.string(),
|
|
27
|
+
updated_by: z.string(),
|
|
28
|
+
}),
|
|
29
|
+
archive: { permission: 'sdoc:archive' },
|
|
30
|
+
stamps: { created_at: 'createdAt', created_by: 'createdBy', updated_at: 'updatedAt', updated_by: 'updatedBy' },
|
|
31
|
+
},
|
|
32
|
+
splain: {
|
|
33
|
+
table: 'stamped_plain',
|
|
34
|
+
fields: z.object({ id: z.string(), title: z.string(), created_at: z.string() }),
|
|
35
|
+
stamps: { created_at: 'createdAt' },
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
/** How an authored write stamps the updated pair: right, or wrong in one particular way. */
|
|
39
|
+
export const STAMP_MODES = ['both', 'skip-at', 'skip-by', 'constant', 'other'];
|
|
40
|
+
const PERMISSIONS = ['sdoc:read', 'sdoc:write', 'sdoc:archive'];
|
|
41
|
+
const doc = (key) => ({ key, entity: 'sdoc', idFrom: 'docId' });
|
|
42
|
+
const byId = z.object({ id: z.string() });
|
|
43
|
+
const touched = { entity: 'sdoc', entityIdFrom: 'id', type: 'sdoc.touched', schemaVersion: 1, piiClass: 'none', payload: ['id'] };
|
|
44
|
+
export const stampsOperations = defineOperations(stampsEntities, PERMISSIONS)({
|
|
45
|
+
'stamps/create-doc': {
|
|
46
|
+
summary: 'Add a doc, every stamp written by the platform',
|
|
47
|
+
derive: 'create',
|
|
48
|
+
permission: 'sdoc:write',
|
|
49
|
+
input: z.object({ title: z.string() }),
|
|
50
|
+
output: stampsEntities.sdoc.fields,
|
|
51
|
+
http: { method: 'POST', path: '/docs' },
|
|
52
|
+
emits: { entity: 'sdoc', entityIdFrom: 'id', type: 'sdoc.created', schemaVersion: 1, piiClass: 'none', payload: ['id', 'title'] },
|
|
53
|
+
},
|
|
54
|
+
'stamps/get-doc': {
|
|
55
|
+
summary: 'One doc',
|
|
56
|
+
derive: 'get',
|
|
57
|
+
permission: doc('sdoc:read'),
|
|
58
|
+
input: z.object({ docId: z.string() }),
|
|
59
|
+
output: stampsEntities.sdoc.fields,
|
|
60
|
+
http: { method: 'GET', path: '/docs/{docId}' },
|
|
61
|
+
},
|
|
62
|
+
'stamps/update-doc': {
|
|
63
|
+
summary: "Change a doc's title",
|
|
64
|
+
derive: 'update',
|
|
65
|
+
permission: doc('sdoc:write'),
|
|
66
|
+
input: z.object({ docId: z.string(), title: z.string().optional() }),
|
|
67
|
+
output: stampsEntities.sdoc.fields,
|
|
68
|
+
http: { method: 'PATCH', path: '/docs/{docId}' },
|
|
69
|
+
concurrency: { over: 'sdoc', idFrom: 'docId' },
|
|
70
|
+
emits: { entity: 'sdoc', entityIdFrom: 'id', type: 'sdoc.updated', schemaVersion: 1, piiClass: 'none', payload: ['id', 'title'] },
|
|
71
|
+
},
|
|
72
|
+
'stamps/archive-doc': {
|
|
73
|
+
summary: 'File a doc away — a kernel move',
|
|
74
|
+
derive: 'archive',
|
|
75
|
+
permission: doc('sdoc:archive'),
|
|
76
|
+
input: z.object({ docId: z.string() }),
|
|
77
|
+
output: entityStateOutput,
|
|
78
|
+
http: { method: 'POST', path: '/docs/{docId}/archive' },
|
|
79
|
+
},
|
|
80
|
+
'stamps/add-doc': {
|
|
81
|
+
summary: 'Add a doc by hand, with or without the updated pair',
|
|
82
|
+
permission: 'sdoc:write',
|
|
83
|
+
input: z.object({ id: z.string(), title: z.string(), stampUpdated: z.boolean() }),
|
|
84
|
+
output: byId,
|
|
85
|
+
emits: { entity: 'sdoc', entityIdFrom: 'id', type: 'sdoc.added', schemaVersion: 1, piiClass: 'none', payload: ['id'] },
|
|
86
|
+
},
|
|
87
|
+
'stamps/touch-doc': {
|
|
88
|
+
summary: "Change a doc's title by hand, stamping as told",
|
|
89
|
+
permission: 'sdoc:write',
|
|
90
|
+
input: z.object({ id: z.string(), title: z.string(), stamp: z.enum(STAMP_MODES) }),
|
|
91
|
+
output: byId,
|
|
92
|
+
emits: touched,
|
|
93
|
+
},
|
|
94
|
+
'stamps/atomic-touch': {
|
|
95
|
+
summary: 'Write a doc without its stamps inside a ctx.atomic that is rolled back',
|
|
96
|
+
permission: 'sdoc:write',
|
|
97
|
+
input: z.object({ id: z.string(), title: z.string() }),
|
|
98
|
+
output: byId,
|
|
99
|
+
emits: touched,
|
|
100
|
+
},
|
|
101
|
+
'stamps/remove-doc': {
|
|
102
|
+
summary: 'Delete a doc by hand',
|
|
103
|
+
permission: 'sdoc:write',
|
|
104
|
+
input: byId,
|
|
105
|
+
output: byId,
|
|
106
|
+
emits: { entity: 'sdoc', entityIdFrom: 'id', type: 'sdoc.removed', schemaVersion: 1, piiClass: 'none', payload: ['id'] },
|
|
107
|
+
},
|
|
108
|
+
'stamps/poke': {
|
|
109
|
+
summary: 'Ask the consumer to write a doc, stamping as told — the event is about the plain row',
|
|
110
|
+
permission: 'sdoc:write',
|
|
111
|
+
input: z.object({ id: z.string(), stamp: z.enum(STAMP_MODES) }),
|
|
112
|
+
output: byId,
|
|
113
|
+
emits: { entity: 'splain', entityIdFrom: 'id', type: 'sdoc.poked', schemaVersion: 1, piiClass: 'none', payload: ['id'] },
|
|
114
|
+
},
|
|
115
|
+
'stamps/history': {
|
|
116
|
+
summary: "How many events name a doc — what a rolled-back write must leave unchanged",
|
|
117
|
+
permission: 'sdoc:read',
|
|
118
|
+
input: byId,
|
|
119
|
+
output: z.object({ events: z.number() }),
|
|
120
|
+
},
|
|
121
|
+
'stamps/touch-plain': {
|
|
122
|
+
summary: 'Write a plain row, which declares no updated stamp',
|
|
123
|
+
permission: 'sdoc:write',
|
|
124
|
+
input: z.object({ id: z.string(), title: z.string() }),
|
|
125
|
+
output: byId,
|
|
126
|
+
emits: { entity: 'splain', entityIdFrom: 'id', type: 'splain.touched', schemaVersion: 1, piiClass: 'none', payload: ['id'] },
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
export const stampsManifest = moduleManifest.parse({
|
|
130
|
+
id: STAMPS_MODULE_ID,
|
|
131
|
+
version: '1.0.0',
|
|
132
|
+
kernelContract: '^0.0.1',
|
|
133
|
+
migrations: { journalDir: './migrations', compatibleFrom: '1.0.0' },
|
|
134
|
+
...manifestOperations(stampsOperations, {
|
|
135
|
+
permissions: { 'sdoc:read': 'read docs', 'sdoc:write': 'write docs', 'sdoc:archive': 'archive docs' },
|
|
136
|
+
consumes: [{ type: 'sdoc.poked', schemaVersion: 1 }],
|
|
137
|
+
}),
|
|
138
|
+
...manifestEntities(stampsEntities, {}),
|
|
139
|
+
entitlementKey: 'stamps',
|
|
140
|
+
});
|
|
141
|
+
const WRITE = permissionKey.parse('sdoc:write');
|
|
142
|
+
const READ = permissionKey.parse('sdoc:read');
|
|
143
|
+
const docRef = (id) => ({ entityType: 'sdoc', entityId: id });
|
|
144
|
+
/** The updated pair as `mode` writes it: this call's, or wrong in one particular way. */
|
|
145
|
+
function updatedPair(ctx, mode) {
|
|
146
|
+
const now = ctx.now();
|
|
147
|
+
const me = String(ctx.principal);
|
|
148
|
+
switch (mode) {
|
|
149
|
+
case 'both':
|
|
150
|
+
return { set: ', updated_at = ?, updated_by = ?', values: [now, me] };
|
|
151
|
+
case 'skip-at':
|
|
152
|
+
return { set: ', updated_by = ?', values: [me] };
|
|
153
|
+
case 'skip-by':
|
|
154
|
+
return { set: ', updated_at = ?', values: [now] };
|
|
155
|
+
case 'constant':
|
|
156
|
+
return { set: ', updated_at = ?, updated_by = ?', values: [STALE_TIME, me] };
|
|
157
|
+
case 'other':
|
|
158
|
+
return { set: ', updated_at = ?, updated_by = ?', values: [now, 'someone-else'] };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function touchDoc(ctx, id, title, mode) {
|
|
162
|
+
const pair = updatedPair(ctx, mode);
|
|
163
|
+
ctx.sql.exec(`UPDATE stamped_docs SET title = ?${pair.set} WHERE id = ?`, [title, ...pair.values, id]);
|
|
164
|
+
ctx.emit({ type: 'sdoc.touched', schemaVersion: 1, entity: docRef(id), piiClass: 'none', payload: { id } });
|
|
165
|
+
}
|
|
166
|
+
export const stampsMod = {
|
|
167
|
+
manifest: stampsManifest,
|
|
168
|
+
migrations: [
|
|
169
|
+
{
|
|
170
|
+
version: '0001-init',
|
|
171
|
+
sql: `CREATE TABLE stamped_docs (id TEXT PRIMARY KEY, title TEXT NOT NULL,
|
|
172
|
+
created_at TEXT NOT NULL, created_by TEXT NOT NULL,
|
|
173
|
+
updated_at TEXT NOT NULL, updated_by TEXT NOT NULL);
|
|
174
|
+
CREATE TABLE stamped_plain (id TEXT PRIMARY KEY, title TEXT NOT NULL, created_at TEXT NOT NULL);`,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
...operationsFor(stampsOperations)({
|
|
178
|
+
'stamps/add-doc': async (ctx, i) => {
|
|
179
|
+
assertAllowed(await ctx.check(WRITE));
|
|
180
|
+
const now = ctx.now();
|
|
181
|
+
const me = String(ctx.principal);
|
|
182
|
+
// Without the pair, an older update's: the row is new, so its updated stamps are this call's too.
|
|
183
|
+
const updated = i.stampUpdated ? [now, me] : [STALE_TIME, me];
|
|
184
|
+
ctx.sql.exec('INSERT INTO stamped_docs (id, title, created_at, created_by, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?)', [i.id, i.title, now, me, ...updated]);
|
|
185
|
+
ctx.emit({ type: 'sdoc.added', schemaVersion: 1, entity: docRef(i.id), piiClass: 'none', payload: { id: i.id } });
|
|
186
|
+
return { id: i.id };
|
|
187
|
+
},
|
|
188
|
+
'stamps/touch-doc': async (ctx, i) => {
|
|
189
|
+
assertAllowed(await ctx.check(WRITE));
|
|
190
|
+
touchDoc(ctx, i.id, i.title, i.stamp);
|
|
191
|
+
return { id: i.id };
|
|
192
|
+
},
|
|
193
|
+
'stamps/atomic-touch': async (ctx, i) => {
|
|
194
|
+
assertAllowed(await ctx.check(WRITE));
|
|
195
|
+
try {
|
|
196
|
+
await ctx.atomic(() => {
|
|
197
|
+
touchDoc(ctx, i.id, i.title, 'skip-at');
|
|
198
|
+
throw new Error('rolled back on purpose');
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// The write and its event are gone; nothing this call commits names the doc.
|
|
203
|
+
}
|
|
204
|
+
return { id: i.id };
|
|
205
|
+
},
|
|
206
|
+
'stamps/remove-doc': async (ctx, i) => {
|
|
207
|
+
assertAllowed(await ctx.check(WRITE));
|
|
208
|
+
ctx.sql.exec('DELETE FROM stamped_docs WHERE id = ?', [i.id]);
|
|
209
|
+
ctx.emit({ type: 'sdoc.removed', schemaVersion: 1, entity: docRef(i.id), piiClass: 'none', payload: { id: i.id } });
|
|
210
|
+
return { id: i.id };
|
|
211
|
+
},
|
|
212
|
+
'stamps/poke': async (ctx, i) => {
|
|
213
|
+
assertAllowed(await ctx.check(WRITE));
|
|
214
|
+
ctx.emit({
|
|
215
|
+
type: 'sdoc.poked',
|
|
216
|
+
schemaVersion: 1,
|
|
217
|
+
entity: { entityType: 'splain', entityId: i.id },
|
|
218
|
+
piiClass: 'none',
|
|
219
|
+
payload: { id: i.id, stamp: i.stamp },
|
|
220
|
+
});
|
|
221
|
+
return { id: i.id };
|
|
222
|
+
},
|
|
223
|
+
'stamps/history': async (ctx, i) => {
|
|
224
|
+
assertAllowed(await ctx.check(READ));
|
|
225
|
+
return { events: readHistory({ sql: ctx.sql }, docRef(i.id), { limit: 100 }).entries.length };
|
|
226
|
+
},
|
|
227
|
+
'stamps/touch-plain': async (ctx, i) => {
|
|
228
|
+
assertAllowed(await ctx.check(WRITE));
|
|
229
|
+
ctx.sql.exec('INSERT INTO stamped_plain (id, title, created_at) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET title = excluded.title', [i.id, i.title, ctx.now()]);
|
|
230
|
+
ctx.emit({ type: 'splain.touched', schemaVersion: 1, entity: { entityType: 'splain', entityId: i.id }, piiClass: 'none', payload: { id: i.id } });
|
|
231
|
+
return { id: i.id };
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
consumers: {
|
|
235
|
+
// Writes the doc the poke names as the consumer's own principal, stamping as the poke says.
|
|
236
|
+
'sdoc.poked': async (ctx, event) => {
|
|
237
|
+
const { id, stamp } = z.object({ id: z.string(), stamp: z.enum(STAMP_MODES) }).parse(event.payload);
|
|
238
|
+
touchDoc(ctx, id, `consumed ${stamp}`, stamp);
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
//# sourceMappingURL=stamps-module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stamps-module.js","sourceRoot":"","sources":["../src/stamps-module.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stamps-suite.d.ts","sourceRoot":"","sources":["../src/stamps-suite.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAM9D,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CA4KlH"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract suite for the updated stamps the host holds true (#2166).
|
|
3
|
+
*
|
|
4
|
+
* What it pins, on each adapter's SQL: an entity that declares `updatedAt`/`updatedBy` holds them on
|
|
5
|
+
* every row an operation or a consumer emits about. A derived create writes all four stamps and a
|
|
6
|
+
* derived update the updated pair. An authored write that stamps the pair passes; one that skips
|
|
7
|
+
* either column, writes a constant time or another principal is refused `internal`, and its row and
|
|
8
|
+
* its event roll back with it. A deleted row, a kernel move, an emit rolled back inside a
|
|
9
|
+
* `ctx.atomic` and an entity without the pair are not checked. A consumer is held to the same rule,
|
|
10
|
+
* with its own instant and principal, and a refused delivery is dead-lettered.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
13
|
+
import { errorCodeOf, permissionKey, platformActorId, principalId, scopeId, tenantId } from '@substrat-run/contracts';
|
|
14
|
+
import { ulid } from '@substrat-run/kernel';
|
|
15
|
+
import { stampsMod } from './stamps-module.js';
|
|
16
|
+
const KEYS = ['sdoc:read', 'sdoc:write', 'sdoc:archive'].map((k) => permissionKey.parse(k));
|
|
17
|
+
export function updatedStampsContractSuite(adapterName, makeFixture) {
|
|
18
|
+
describe(`updated stamps held by the host (#2166): ${adapterName}`, () => {
|
|
19
|
+
let fixture;
|
|
20
|
+
let host;
|
|
21
|
+
const t = tenantId.parse(ulid());
|
|
22
|
+
const s = scopeId.parse(ulid());
|
|
23
|
+
const staff = platformActorId.parse(ulid());
|
|
24
|
+
const alice = principalId.parse(ulid());
|
|
25
|
+
const carl = principalId.parse(ulid());
|
|
26
|
+
let as;
|
|
27
|
+
let asCarl;
|
|
28
|
+
const refusal = (call) => call.then(() => ({ code: 'answered', message: '' }), (e) => ({ code: errorCodeOf(e), message: e.message }));
|
|
29
|
+
const get = (id) => as.invoke('stamps/get-doc', { docId: id });
|
|
30
|
+
const events = async (id) => (await as.invoke('stamps/history', { id })).events;
|
|
31
|
+
const create = async (title) => as.invoke('stamps/create-doc', { title });
|
|
32
|
+
/**
|
|
33
|
+
* A stamp left alone is only stale once the clock has moved past it: in the same millisecond
|
|
34
|
+
* the old time IS this call's, and true. A timer, because workerd's clock stands still while
|
|
35
|
+
* code runs.
|
|
36
|
+
*/
|
|
37
|
+
const pastStamp = async (d) => {
|
|
38
|
+
while (new Date().toISOString() <= d.updated_at)
|
|
39
|
+
await new Promise((r) => setTimeout(r, 2));
|
|
40
|
+
};
|
|
41
|
+
beforeAll(async () => {
|
|
42
|
+
fixture = await makeFixture();
|
|
43
|
+
host = fixture.host;
|
|
44
|
+
host.registerModule(stampsMod);
|
|
45
|
+
await host.admin.createTenant(staff, { id: t, slug: 'stamps-tenant', name: 'Stamps Tenant' });
|
|
46
|
+
await host.admin.grantEntitlement(staff, t, 'stamps');
|
|
47
|
+
await host.admin.defineRole(staff, t, { key: 'stamps-admin', permissions: KEYS, source: 'vertical' });
|
|
48
|
+
for (const who of [alice, carl]) {
|
|
49
|
+
await host.admin.assignRole(staff, { principalId: who, roleKey: 'stamps-admin', node: { tenantId: t, scopeId: null } });
|
|
50
|
+
}
|
|
51
|
+
await host.provisionScope(staff, { tenantId: t, scopeId: s, vertical: 'stamps-vertical' });
|
|
52
|
+
await host.admin.activateScope(staff, t, s);
|
|
53
|
+
as = await host.getScope(alice, t, s);
|
|
54
|
+
asCarl = await host.getScope(carl, t, s);
|
|
55
|
+
});
|
|
56
|
+
afterAll(async () => {
|
|
57
|
+
await fixture.cleanup();
|
|
58
|
+
});
|
|
59
|
+
describe('derived writes stamp the pair themselves', () => {
|
|
60
|
+
it('a derived create writes all four stamps: the call’s instant and the caller', async () => {
|
|
61
|
+
const before = new Date().toISOString();
|
|
62
|
+
const d = await create('first');
|
|
63
|
+
const after = new Date().toISOString();
|
|
64
|
+
expect(d.created_at).toBe(d.updated_at);
|
|
65
|
+
expect(d.created_at >= before && d.created_at <= after).toBe(true);
|
|
66
|
+
expect([d.created_by, d.updated_by]).toEqual([alice, alice]);
|
|
67
|
+
expect(await get(d.id)).toEqual(d);
|
|
68
|
+
});
|
|
69
|
+
it('a derived update moves the updated pair to its own caller, and leaves the created pair', async () => {
|
|
70
|
+
const d = await create('second');
|
|
71
|
+
const before = new Date().toISOString();
|
|
72
|
+
const after = await asCarl.invoke('stamps/update-doc', { docId: d.id, title: 'changed' });
|
|
73
|
+
expect(after).toEqual({ ...d, title: 'changed', updated_at: after.updated_at, updated_by: carl });
|
|
74
|
+
expect(after.updated_at >= before).toBe(true);
|
|
75
|
+
expect(await get(d.id)).toEqual(after);
|
|
76
|
+
});
|
|
77
|
+
it('a derived update that sends nothing writes nothing, so the pair stays', async () => {
|
|
78
|
+
const d = await create('untouched');
|
|
79
|
+
expect(await asCarl.invoke('stamps/update-doc', { docId: d.id })).toEqual(d);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('an authored write is held to the pair', () => {
|
|
83
|
+
it('an authored update that stamps the pair passes', async () => {
|
|
84
|
+
const d = await create('authored');
|
|
85
|
+
await asCarl.invoke('stamps/touch-doc', { id: d.id, title: 'by hand', stamp: 'both' });
|
|
86
|
+
const after = await get(d.id);
|
|
87
|
+
expect(after.title).toBe('by hand');
|
|
88
|
+
expect(after.updated_by).toBe(carl);
|
|
89
|
+
expect(after.created_by).toBe(alice);
|
|
90
|
+
});
|
|
91
|
+
it.each([
|
|
92
|
+
['skip-at', 'updated_at', 'time'],
|
|
93
|
+
['skip-by', 'updated_by', 'principal'],
|
|
94
|
+
['constant', 'updated_at', 'time'],
|
|
95
|
+
['other', 'updated_by', 'principal'],
|
|
96
|
+
])('stamping %s is refused internal, and the write and its event roll back', async (stamp, column, what) => {
|
|
97
|
+
const d = await create(`refused ${stamp}`);
|
|
98
|
+
await pastStamp(d);
|
|
99
|
+
const seen = await events(d.id);
|
|
100
|
+
const r = await refusal(asCarl.invoke('stamps/touch-doc', { id: d.id, title: 'lost', stamp }));
|
|
101
|
+
expect(r.code).toBe('internal');
|
|
102
|
+
expect(r.message).toContain(`'${column}' does not hold this call's ${what}`);
|
|
103
|
+
expect(r.message).toContain('stamps/touch-doc');
|
|
104
|
+
expect(await get(d.id)).toEqual(d);
|
|
105
|
+
expect(await events(d.id)).toBe(seen);
|
|
106
|
+
});
|
|
107
|
+
it('an authored insert must stamp the updated pair too: without it, no row is left', async () => {
|
|
108
|
+
const ok = ulid();
|
|
109
|
+
await as.invoke('stamps/add-doc', { id: ok, title: 'with', stampUpdated: true });
|
|
110
|
+
expect((await get(ok)).updated_at).toBe((await get(ok)).created_at);
|
|
111
|
+
const stale = ulid();
|
|
112
|
+
const r = await refusal(as.invoke('stamps/add-doc', { id: stale, title: 'without', stampUpdated: false }));
|
|
113
|
+
expect(r.code).toBe('internal');
|
|
114
|
+
expect((await refusal(get(stale))).code).toBe('not_found');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe('what is not checked', () => {
|
|
118
|
+
it('an emit about a deleted row: there is nothing left to stamp', async () => {
|
|
119
|
+
const d = await create('to remove');
|
|
120
|
+
await as.invoke('stamps/remove-doc', { id: d.id });
|
|
121
|
+
expect((await refusal(get(d.id))).code).toBe('not_found');
|
|
122
|
+
});
|
|
123
|
+
it('a kernel move: archiving writes only the platform’s columns, and leaves the pair', async () => {
|
|
124
|
+
const d = await create('to archive');
|
|
125
|
+
await asCarl.invoke('stamps/archive-doc', { docId: d.id });
|
|
126
|
+
expect(await get(d.id)).toEqual(d);
|
|
127
|
+
});
|
|
128
|
+
it('an emit rolled back inside ctx.atomic is gone from the outbox, so it is not held', async () => {
|
|
129
|
+
const d = await create('atomic');
|
|
130
|
+
// Another principal, past the create's millisecond: the rolled-back write left the row
|
|
131
|
+
// stale in time, so were its emit still counted, this call would be refused.
|
|
132
|
+
await pastStamp(d);
|
|
133
|
+
const seen = await events(d.id);
|
|
134
|
+
await asCarl.invoke('stamps/atomic-touch', { id: d.id, title: 'never' });
|
|
135
|
+
expect(await get(d.id)).toEqual(d);
|
|
136
|
+
expect(await events(d.id)).toBe(seen);
|
|
137
|
+
});
|
|
138
|
+
it('an entity that declares no updated stamp: an unstamped write passes', async () => {
|
|
139
|
+
const id = ulid();
|
|
140
|
+
await expect(as.invoke('stamps/touch-plain', { id, title: 'one' })).resolves.toEqual({ id });
|
|
141
|
+
await expect(asCarl.invoke('stamps/touch-plain', { id, title: 'two' })).resolves.toEqual({ id });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('a consumer is held to the pair, as its own principal', () => {
|
|
145
|
+
const poke = async (stamp) => {
|
|
146
|
+
const d = await create(`poked ${stamp}`);
|
|
147
|
+
await as.invoke('stamps/poke', { id: d.id, stamp });
|
|
148
|
+
return d;
|
|
149
|
+
};
|
|
150
|
+
const lettersFor = async (id) => (await host.admin.deadLetters(staff, t, s, {})).entries.filter((l) => String(l.error ?? '').includes(id));
|
|
151
|
+
it('a consumer that stamps the pair writes the row, as its own principal', async () => {
|
|
152
|
+
const d = await poke('both');
|
|
153
|
+
const after = await get(d.id);
|
|
154
|
+
expect(after.title).toBe('consumed both');
|
|
155
|
+
expect(after.updated_by).not.toBe(alice);
|
|
156
|
+
expect(after.updated_at >= d.updated_at).toBe(true);
|
|
157
|
+
expect(await lettersFor(d.id)).toEqual([]);
|
|
158
|
+
});
|
|
159
|
+
it('a consumer that skips the pair is dead-lettered, and its write rolls back', async () => {
|
|
160
|
+
const d = await poke('constant');
|
|
161
|
+
expect(await get(d.id)).toEqual(d);
|
|
162
|
+
const letters = await lettersFor(d.id);
|
|
163
|
+
expect(letters).toHaveLength(1);
|
|
164
|
+
expect(String(letters[0]?.error)).toContain("'updated_at' does not hold this call's time");
|
|
165
|
+
expect(String(letters[0]?.error)).toContain('consuming sdoc.poked');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=stamps-suite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stamps-suite.js","sourceRoot":"","sources":["../src/stamps-suite.ts"],"names":[],"mappings":"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"}
|
|
@@ -106,6 +106,7 @@ export declare const crmExportModManifest: {
|
|
|
106
106
|
archivePermission?: (string & z.$brand<"PermissionKey">) | undefined;
|
|
107
107
|
trashPermission?: (string & z.$brand<"PermissionKey">) | undefined;
|
|
108
108
|
purgeAfterDays?: number | undefined;
|
|
109
|
+
purgeEarly?: boolean | undefined;
|
|
109
110
|
table?: string | undefined;
|
|
110
111
|
idColumn?: string | undefined;
|
|
111
112
|
entityType: string;
|
|
@@ -123,6 +124,13 @@ export declare const crmExportModManifest: {
|
|
|
123
124
|
}[];
|
|
124
125
|
}[];
|
|
125
126
|
} | undefined;
|
|
127
|
+
updatedStamps?: {
|
|
128
|
+
table: string;
|
|
129
|
+
idColumn: string;
|
|
130
|
+
at?: string | undefined;
|
|
131
|
+
by?: string | undefined;
|
|
132
|
+
entityType: string;
|
|
133
|
+
}[] | undefined;
|
|
126
134
|
ui?: {
|
|
127
135
|
routes?: {
|
|
128
136
|
path: string;
|
|
@@ -255,6 +263,7 @@ export declare const boardImportModManifest: {
|
|
|
255
263
|
archivePermission?: (string & z.$brand<"PermissionKey">) | undefined;
|
|
256
264
|
trashPermission?: (string & z.$brand<"PermissionKey">) | undefined;
|
|
257
265
|
purgeAfterDays?: number | undefined;
|
|
266
|
+
purgeEarly?: boolean | undefined;
|
|
258
267
|
table?: string | undefined;
|
|
259
268
|
idColumn?: string | undefined;
|
|
260
269
|
entityType: string;
|
|
@@ -272,6 +281,13 @@ export declare const boardImportModManifest: {
|
|
|
272
281
|
}[];
|
|
273
282
|
}[];
|
|
274
283
|
} | undefined;
|
|
284
|
+
updatedStamps?: {
|
|
285
|
+
table: string;
|
|
286
|
+
idColumn: string;
|
|
287
|
+
at?: string | undefined;
|
|
288
|
+
by?: string | undefined;
|
|
289
|
+
entityType: string;
|
|
290
|
+
}[] | undefined;
|
|
275
291
|
ui?: {
|
|
276
292
|
routes?: {
|
|
277
293
|
path: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vertical-events-suite.d.ts","sourceRoot":"","sources":["../src/vertical-events-suite.ts"],"names":[],"mappings":"AACA,OAAO,EASL,CAAC,EAED,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EAGtB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACd,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAOL,KAAK,kBAAkB,EAEvB,KAAK,kBAAkB,EAGvB,KAAK,SAAS,EAEf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAA4D,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAevH,eAAO,MAAM,YAAY,aAAa,CAAC;AACvC,eAAO,MAAM,cAAc,eAAe,CAAC;AAI3C,eAAO,MAAM,oBAAoB
|
|
1
|
+
{"version":3,"file":"vertical-events-suite.d.ts","sourceRoot":"","sources":["../src/vertical-events-suite.ts"],"names":[],"mappings":"AACA,OAAO,EASL,CAAC,EAED,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EAGtB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACd,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAOL,KAAK,kBAAkB,EAEvB,KAAK,kBAAkB,EAGvB,KAAK,SAAS,EAEf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAA4D,KAAK,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAevH,eAAO,MAAM,YAAY,aAAa,CAAC;AACvC,eAAO,MAAM,cAAc,eAAe,CAAC;AAI3C,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiC/B,CAAC;AAiFH,eAAO,MAAM,YAAY,EAAE,kBAuC1B,CAAC;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCjC,CAAC;AAgDH,eAAO,MAAM,cAAc,EAAE,kBAmE5B,CAAC;AAIF;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,YAAY,CAAC,CAAC;IACnD;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACrG;;;OAGG;IACH,IAAI,CAAC,EAAE,CACL,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,OAAO,KACb,OAAO,CAAC,SAAS;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE;YAAE,MAAM,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,EAAE,CAAC,CAAC;IACtG;;;;OAIG;IACH,eAAe,CAAC,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CACpD;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,OAAO,CAAC,qBAAqB,CAAC,GAChD,IAAI,CA65CN"}
|
|
@@ -194,6 +194,8 @@ export const boardImportModManifest = moduleManifest.parse({
|
|
|
194
194
|
peers: [{ vertical: CRM_VERTICAL, operations: [], permissions: ['association:sync', 'association:read'] }],
|
|
195
195
|
migrations: { journalDir: './migrations', compatibleFrom: '1.0.0' },
|
|
196
196
|
attachmentTargets: [],
|
|
197
|
+
// #2166: an association holds its updated stamps, so an IMPORT handler is held to them too.
|
|
198
|
+
updatedStamps: [{ entityType: 'association', table: 'board_associations', idColumn: 'crm_id', at: 'updated_at', by: 'updated_by' }],
|
|
197
199
|
entitlementKey: 'board-import',
|
|
198
200
|
});
|
|
199
201
|
const customerCreated = z.object({ id: z.string(), name: z.string() });
|
|
@@ -228,7 +230,7 @@ export const boardImportMod = {
|
|
|
228
230
|
{
|
|
229
231
|
version: '0001-init',
|
|
230
232
|
sql: `
|
|
231
|
-
CREATE TABLE board_associations (crm_id TEXT PRIMARY KEY, name TEXT NOT NULL);
|
|
233
|
+
CREATE TABLE board_associations (crm_id TEXT PRIMARY KEY, name TEXT NOT NULL, updated_at TEXT, updated_by TEXT);
|
|
232
234
|
CREATE TABLE board_noted (event_id TEXT PRIMARY KEY);
|
|
233
235
|
`,
|
|
234
236
|
},
|
|
@@ -251,8 +253,11 @@ export const boardImportMod = {
|
|
|
251
253
|
// The name is quoted, so a handler's own sentence can carry a subject's id (#1757).
|
|
252
254
|
if (c.name.startsWith('poison'))
|
|
253
255
|
throw new Error(`board refuses the poison customer ${c.name}`);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
+
// #2166: a 'stale' customer is written with a time that is not this delivery's.
|
|
257
|
+
const at = c.name.startsWith('stale') ? '2000-01-01T00:00:00.000Z' : ctx.now();
|
|
258
|
+
ctx.sql.exec(`INSERT INTO board_associations (crm_id, name, updated_at, updated_by) VALUES (?, ?, ?, ?)
|
|
259
|
+
ON CONFLICT (crm_id) DO UPDATE SET name = excluded.name, updated_at = excluded.updated_at,
|
|
260
|
+
updated_by = excluded.updated_by`, [c.id, c.name, at, String(ctx.principal)]);
|
|
256
261
|
ctx.emit({
|
|
257
262
|
type: 'board.association-created',
|
|
258
263
|
schemaVersion: 1,
|
|
@@ -268,6 +273,12 @@ export const boardImportMod = {
|
|
|
268
273
|
'crm.customer-touched': async (ctx, event) => {
|
|
269
274
|
assertAllowed(await ctx.check(key('association:sync')));
|
|
270
275
|
const { id } = z.object({ id: z.string() }).parse(event.payload);
|
|
276
|
+
// #2166: an event about an association is a write to it, so it moves the stamps.
|
|
277
|
+
ctx.sql.exec('UPDATE board_associations SET updated_at = ?, updated_by = ? WHERE crm_id = ?', [
|
|
278
|
+
ctx.now(),
|
|
279
|
+
String(ctx.principal),
|
|
280
|
+
id,
|
|
281
|
+
]);
|
|
271
282
|
ctx.emit({
|
|
272
283
|
type: 'board.association-linked',
|
|
273
284
|
schemaVersion: 1,
|
|
@@ -582,6 +593,22 @@ export function verticalEventsContractSuite(adapterName, makeFixture) {
|
|
|
582
593
|
expect(refused).toBeTruthy();
|
|
583
594
|
expect(poison).toBeTruthy();
|
|
584
595
|
});
|
|
596
|
+
it('an import handler is held to the updated stamps (#2166): a stale one is dead-lettered and rolls back', async () => {
|
|
597
|
+
const t = await newTenant();
|
|
598
|
+
const p = await install(t, CRM_VERTICAL);
|
|
599
|
+
const c = await install(t, BOARD_VERTICAL);
|
|
600
|
+
await create(t, p, 'stale customer');
|
|
601
|
+
await create(t, p, 'Fresh customer');
|
|
602
|
+
const { report } = await sweep();
|
|
603
|
+
expect(into(report, c)).toMatchObject({ state: 'delivered', delivered: 1, deadLettered: 1 });
|
|
604
|
+
const view = await board(t, c);
|
|
605
|
+
expect(view.associations.map((r) => r.name)).toEqual(['Fresh customer']);
|
|
606
|
+
const refused = view.deliveries.filter((d) => d.error?.includes("'updated_at' does not hold this call's time"));
|
|
607
|
+
expect(refused).toHaveLength(1);
|
|
608
|
+
expect(refused[0]?.error).toContain('consuming crm.customer-created');
|
|
609
|
+
// Its event rolled back with its row.
|
|
610
|
+
expect(view.outbox.filter((o) => o.type === 'board.association-created')).toHaveLength(1);
|
|
611
|
+
});
|
|
585
612
|
it('at-least-once across the edge: a batch delivered twice runs each handler once', async () => {
|
|
586
613
|
const t = await newTenant();
|
|
587
614
|
const p = await install(t, CRM_VERTICAL);
|