@peers-app/peers-sdk 0.16.4 → 0.16.6
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/context/user-context.js +6 -0
- package/dist/contracts/__tests__/builder.test.d.ts +1 -0
- package/dist/contracts/__tests__/builder.test.js +411 -0
- package/dist/contracts/__tests__/extract.test.d.ts +1 -0
- package/dist/contracts/__tests__/extract.test.js +145 -0
- package/dist/contracts/__tests__/integration.test.d.ts +1 -0
- package/dist/contracts/__tests__/integration.test.js +348 -0
- package/dist/contracts/__tests__/registry.test.d.ts +1 -0
- package/dist/contracts/__tests__/registry.test.js +324 -0
- package/dist/contracts/__tests__/validate.test.d.ts +1 -0
- package/dist/contracts/__tests__/validate.test.js +699 -0
- package/dist/contracts/builder.d.ts +97 -0
- package/dist/contracts/builder.js +211 -0
- package/dist/contracts/contract-providers.table.d.ts +40 -0
- package/dist/contracts/contract-providers.table.js +41 -0
- package/dist/contracts/contracts.table.d.ts +44 -0
- package/dist/contracts/contracts.table.js +44 -0
- package/dist/contracts/extract.d.ts +46 -0
- package/dist/contracts/extract.js +51 -0
- package/dist/contracts/index.d.ts +9 -0
- package/dist/contracts/index.js +31 -0
- package/dist/contracts/persistent-registry.d.ts +32 -0
- package/dist/contracts/persistent-registry.js +138 -0
- package/dist/contracts/registry.d.ts +58 -0
- package/dist/contracts/registry.js +155 -0
- package/dist/contracts/types.d.ts +108 -0
- package/dist/contracts/types.js +10 -0
- package/dist/contracts/validate.d.ts +24 -0
- package/dist/contracts/validate.js +274 -0
- package/dist/data/assistants.d.ts +5 -0
- package/dist/data/assistants.js +1 -0
- package/dist/data/package-versions.d.ts +2 -2
- package/dist/data/persistent-vars.d.ts +3 -0
- package/dist/data/persistent-vars.js +17 -2
- package/dist/data/tools.d.ts +4 -0
- package/dist/data/tools.js +1 -0
- package/dist/data/workflows.d.ts +1 -0
- package/dist/events.d.ts +13 -0
- package/dist/events.js +218 -34
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/package-loader/contract-package-loader.d.ts +23 -0
- package/dist/package-loader/contract-package-loader.js +65 -0
- package/dist/package-loader/index.d.ts +1 -0
- package/dist/package-loader/index.js +1 -0
- package/dist/package-loader/package-loader.d.ts +11 -0
- package/dist/package-loader/package-loader.js +59 -8
- package/dist/rpc-types.d.ts +4 -0
- package/dist/rpc-types.js +5 -4
- package/dist/types/workflow.d.ts +3 -0
- package/dist/types/workflow.js +1 -0
- package/dist/utils.d.ts +15 -0
- package/dist/utils.js +39 -0
- package/package.json +1 -1
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const field_type_1 = require("../../types/field-type");
|
|
4
|
+
const utils_1 = require("../../utils");
|
|
5
|
+
const types_1 = require("../types");
|
|
6
|
+
const validate_1 = require("../validate");
|
|
7
|
+
const DEFAULT_CONTRACT_ID = (0, utils_1.newid)();
|
|
8
|
+
/** Helper to build a minimal contract definition for tests. */
|
|
9
|
+
function makeContract(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
contractId: DEFAULT_CONTRACT_ID,
|
|
12
|
+
version: 1,
|
|
13
|
+
name: "Test Contract",
|
|
14
|
+
description: "A test contract",
|
|
15
|
+
tables: [],
|
|
16
|
+
tools: [],
|
|
17
|
+
observables: [],
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
describe("validateProviderSatisfiesContract", () => {
|
|
22
|
+
describe("tables", () => {
|
|
23
|
+
it("passes when implementation has the same fields as the contract", () => {
|
|
24
|
+
const contract = makeContract({
|
|
25
|
+
tables: [
|
|
26
|
+
{
|
|
27
|
+
name: "Tasks",
|
|
28
|
+
description: "Tasks table",
|
|
29
|
+
primaryKeyName: "taskId",
|
|
30
|
+
fields: [
|
|
31
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
32
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
const impl = makeContract({ tables: [...contract.tables] });
|
|
38
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
39
|
+
expect(result.valid).toBe(true);
|
|
40
|
+
expect(result.errors).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
it("passes when implementation has extra optional fields", () => {
|
|
43
|
+
const contract = makeContract({
|
|
44
|
+
tables: [
|
|
45
|
+
{
|
|
46
|
+
name: "Tasks",
|
|
47
|
+
description: "Tasks table",
|
|
48
|
+
primaryKeyName: "taskId",
|
|
49
|
+
fields: [
|
|
50
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
51
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
const impl = makeContract({
|
|
57
|
+
tables: [
|
|
58
|
+
{
|
|
59
|
+
name: "Tasks",
|
|
60
|
+
description: "Tasks table",
|
|
61
|
+
primaryKeyName: "taskId",
|
|
62
|
+
fields: [
|
|
63
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
64
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
65
|
+
{ name: "priority", type: field_type_1.FieldType.number, optional: true },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
71
|
+
expect(result.valid).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it("fails when implementation is missing a required field", () => {
|
|
74
|
+
const contract = makeContract({
|
|
75
|
+
tables: [
|
|
76
|
+
{
|
|
77
|
+
name: "Tasks",
|
|
78
|
+
description: "Tasks table",
|
|
79
|
+
primaryKeyName: "taskId",
|
|
80
|
+
fields: [
|
|
81
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
82
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
83
|
+
{ name: "status", type: field_type_1.FieldType.string },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
const impl = makeContract({
|
|
89
|
+
tables: [
|
|
90
|
+
{
|
|
91
|
+
name: "Tasks",
|
|
92
|
+
description: "Tasks table",
|
|
93
|
+
primaryKeyName: "taskId",
|
|
94
|
+
fields: [
|
|
95
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
96
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
102
|
+
expect(result.valid).toBe(false);
|
|
103
|
+
expect(result.errors).toHaveLength(1);
|
|
104
|
+
expect(result.errors[0].message).toContain("status");
|
|
105
|
+
expect(result.errors[0].message).toContain("missing");
|
|
106
|
+
});
|
|
107
|
+
it("allows missing optional contract fields in the implementation", () => {
|
|
108
|
+
const contract = makeContract({
|
|
109
|
+
tables: [
|
|
110
|
+
{
|
|
111
|
+
name: "Tasks",
|
|
112
|
+
description: "Tasks table",
|
|
113
|
+
primaryKeyName: "taskId",
|
|
114
|
+
fields: [
|
|
115
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
116
|
+
{ name: "notes", type: field_type_1.FieldType.string, optional: true },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
const impl = makeContract({
|
|
122
|
+
tables: [
|
|
123
|
+
{
|
|
124
|
+
name: "Tasks",
|
|
125
|
+
description: "Tasks table",
|
|
126
|
+
primaryKeyName: "taskId",
|
|
127
|
+
fields: [{ name: "taskId", type: field_type_1.FieldType.id }],
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
132
|
+
expect(result.valid).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
it("fails when a field has the wrong type", () => {
|
|
135
|
+
const contract = makeContract({
|
|
136
|
+
tables: [
|
|
137
|
+
{
|
|
138
|
+
name: "Tasks",
|
|
139
|
+
description: "Tasks table",
|
|
140
|
+
primaryKeyName: "taskId",
|
|
141
|
+
fields: [
|
|
142
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
143
|
+
{ name: "count", type: field_type_1.FieldType.number },
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
const impl = makeContract({
|
|
149
|
+
tables: [
|
|
150
|
+
{
|
|
151
|
+
name: "Tasks",
|
|
152
|
+
description: "Tasks table",
|
|
153
|
+
primaryKeyName: "taskId",
|
|
154
|
+
fields: [
|
|
155
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
156
|
+
{ name: "count", type: field_type_1.FieldType.string },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
162
|
+
expect(result.valid).toBe(false);
|
|
163
|
+
expect(result.errors[0].message).toContain("type mismatch");
|
|
164
|
+
});
|
|
165
|
+
it("fails when contract requires required but implementation has optional", () => {
|
|
166
|
+
const contract = makeContract({
|
|
167
|
+
tables: [
|
|
168
|
+
{
|
|
169
|
+
name: "Tasks",
|
|
170
|
+
description: "d",
|
|
171
|
+
primaryKeyName: "taskId",
|
|
172
|
+
fields: [{ name: "title", type: field_type_1.FieldType.string }],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
});
|
|
176
|
+
const impl = makeContract({
|
|
177
|
+
tables: [
|
|
178
|
+
{
|
|
179
|
+
name: "Tasks",
|
|
180
|
+
description: "d",
|
|
181
|
+
primaryKeyName: "taskId",
|
|
182
|
+
fields: [{ name: "title", type: field_type_1.FieldType.string, optional: true }],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
187
|
+
expect(result.valid).toBe(false);
|
|
188
|
+
expect(result.errors[0].message).toContain("optional");
|
|
189
|
+
});
|
|
190
|
+
it("fails when contract table is entirely missing from implementation", () => {
|
|
191
|
+
const contract = makeContract({
|
|
192
|
+
tables: [
|
|
193
|
+
{
|
|
194
|
+
name: "Tasks",
|
|
195
|
+
description: "d",
|
|
196
|
+
primaryKeyName: "taskId",
|
|
197
|
+
fields: [{ name: "taskId", type: field_type_1.FieldType.id }],
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
const impl = makeContract({ tables: [] });
|
|
202
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
203
|
+
expect(result.valid).toBe(false);
|
|
204
|
+
expect(result.errors[0].kind).toBe("table");
|
|
205
|
+
expect(result.errors[0].message).toContain("not provided");
|
|
206
|
+
});
|
|
207
|
+
it("fails when primary key names differ", () => {
|
|
208
|
+
const contract = makeContract({
|
|
209
|
+
tables: [
|
|
210
|
+
{
|
|
211
|
+
name: "Tasks",
|
|
212
|
+
description: "d",
|
|
213
|
+
primaryKeyName: "taskId",
|
|
214
|
+
fields: [{ name: "taskId", type: field_type_1.FieldType.id }],
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
const impl = makeContract({
|
|
219
|
+
tables: [
|
|
220
|
+
{
|
|
221
|
+
name: "Tasks",
|
|
222
|
+
description: "d",
|
|
223
|
+
primaryKeyName: "id",
|
|
224
|
+
fields: [{ name: "id", type: field_type_1.FieldType.id }],
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
229
|
+
expect(result.valid).toBe(false);
|
|
230
|
+
expect(result.errors[0].message).toContain("primary key");
|
|
231
|
+
});
|
|
232
|
+
it("fails when implementation has extra required fields not in contract", () => {
|
|
233
|
+
const contract = makeContract({
|
|
234
|
+
tables: [
|
|
235
|
+
{
|
|
236
|
+
name: "Tasks",
|
|
237
|
+
description: "d",
|
|
238
|
+
primaryKeyName: "taskId",
|
|
239
|
+
fields: [{ name: "taskId", type: field_type_1.FieldType.id }],
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
});
|
|
243
|
+
const impl = makeContract({
|
|
244
|
+
tables: [
|
|
245
|
+
{
|
|
246
|
+
name: "Tasks",
|
|
247
|
+
description: "d",
|
|
248
|
+
primaryKeyName: "taskId",
|
|
249
|
+
fields: [
|
|
250
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
251
|
+
{ name: "internalField", type: field_type_1.FieldType.string },
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
});
|
|
256
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
257
|
+
expect(result.valid).toBe(false);
|
|
258
|
+
expect(result.errors[0].message).toContain("extra required field");
|
|
259
|
+
});
|
|
260
|
+
it("validates isArray compatibility", () => {
|
|
261
|
+
const contract = makeContract({
|
|
262
|
+
tables: [
|
|
263
|
+
{
|
|
264
|
+
name: "T",
|
|
265
|
+
description: "d",
|
|
266
|
+
primaryKeyName: "id",
|
|
267
|
+
fields: [
|
|
268
|
+
{ name: "id", type: field_type_1.FieldType.id },
|
|
269
|
+
{ name: "tags", type: field_type_1.FieldType.string, isArray: true },
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
});
|
|
274
|
+
const impl = makeContract({
|
|
275
|
+
tables: [
|
|
276
|
+
{
|
|
277
|
+
name: "T",
|
|
278
|
+
description: "d",
|
|
279
|
+
primaryKeyName: "id",
|
|
280
|
+
fields: [
|
|
281
|
+
{ name: "id", type: field_type_1.FieldType.id },
|
|
282
|
+
{ name: "tags", type: field_type_1.FieldType.string },
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
});
|
|
287
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
288
|
+
expect(result.valid).toBe(false);
|
|
289
|
+
expect(result.errors[0].message).toContain("array");
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
describe("tools", () => {
|
|
293
|
+
it("passes when tool signatures match", () => {
|
|
294
|
+
const contract = makeContract({
|
|
295
|
+
tools: [
|
|
296
|
+
{
|
|
297
|
+
name: "create-task",
|
|
298
|
+
usageDescription: "Creates a task",
|
|
299
|
+
inputFields: [{ name: "title", type: field_type_1.FieldType.string }],
|
|
300
|
+
outputFields: [{ name: "taskId", type: field_type_1.FieldType.id }],
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
});
|
|
304
|
+
const impl = makeContract({ tools: [...contract.tools] });
|
|
305
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
306
|
+
expect(result.valid).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
it("fails when a required tool is missing", () => {
|
|
309
|
+
const contract = makeContract({
|
|
310
|
+
tools: [
|
|
311
|
+
{
|
|
312
|
+
name: "create-task",
|
|
313
|
+
usageDescription: "Creates a task",
|
|
314
|
+
inputFields: [],
|
|
315
|
+
outputFields: [],
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
});
|
|
319
|
+
const impl = makeContract({ tools: [] });
|
|
320
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
321
|
+
expect(result.valid).toBe(false);
|
|
322
|
+
expect(result.errors[0].kind).toBe("tool");
|
|
323
|
+
});
|
|
324
|
+
it("allows extra optional input fields in implementation", () => {
|
|
325
|
+
const contract = makeContract({
|
|
326
|
+
tools: [
|
|
327
|
+
{
|
|
328
|
+
name: "create-task",
|
|
329
|
+
usageDescription: "d",
|
|
330
|
+
inputFields: [{ name: "title", type: field_type_1.FieldType.string }],
|
|
331
|
+
outputFields: [],
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
});
|
|
335
|
+
const impl = makeContract({
|
|
336
|
+
tools: [
|
|
337
|
+
{
|
|
338
|
+
name: "create-task",
|
|
339
|
+
usageDescription: "d",
|
|
340
|
+
inputFields: [
|
|
341
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
342
|
+
{ name: "priority", type: field_type_1.FieldType.number, optional: true },
|
|
343
|
+
],
|
|
344
|
+
outputFields: [],
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
});
|
|
348
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
349
|
+
expect(result.valid).toBe(true);
|
|
350
|
+
});
|
|
351
|
+
it("fails when implementation has extra required input fields", () => {
|
|
352
|
+
const contract = makeContract({
|
|
353
|
+
tools: [
|
|
354
|
+
{
|
|
355
|
+
name: "create-task",
|
|
356
|
+
usageDescription: "d",
|
|
357
|
+
inputFields: [{ name: "title", type: field_type_1.FieldType.string }],
|
|
358
|
+
outputFields: [],
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
});
|
|
362
|
+
const impl = makeContract({
|
|
363
|
+
tools: [
|
|
364
|
+
{
|
|
365
|
+
name: "create-task",
|
|
366
|
+
usageDescription: "d",
|
|
367
|
+
inputFields: [
|
|
368
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
369
|
+
{ name: "mandatory", type: field_type_1.FieldType.string },
|
|
370
|
+
],
|
|
371
|
+
outputFields: [],
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
});
|
|
375
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
376
|
+
expect(result.valid).toBe(false);
|
|
377
|
+
expect(result.errors[0].message).toContain("extra required input");
|
|
378
|
+
});
|
|
379
|
+
it("allows extra output fields in implementation (returns more data)", () => {
|
|
380
|
+
const contract = makeContract({
|
|
381
|
+
tools: [
|
|
382
|
+
{
|
|
383
|
+
name: "create-task",
|
|
384
|
+
usageDescription: "d",
|
|
385
|
+
inputFields: [],
|
|
386
|
+
outputFields: [{ name: "taskId", type: field_type_1.FieldType.id }],
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
});
|
|
390
|
+
const impl = makeContract({
|
|
391
|
+
tools: [
|
|
392
|
+
{
|
|
393
|
+
name: "create-task",
|
|
394
|
+
usageDescription: "d",
|
|
395
|
+
inputFields: [],
|
|
396
|
+
outputFields: [
|
|
397
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
398
|
+
{ name: "createdAt", type: field_type_1.FieldType.Date },
|
|
399
|
+
],
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
});
|
|
403
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
404
|
+
expect(result.valid).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
describe("observables", () => {
|
|
408
|
+
it("passes when observables match", () => {
|
|
409
|
+
const contract = makeContract({
|
|
410
|
+
observables: [
|
|
411
|
+
{ name: "taskCount", description: "d", valueType: field_type_1.FieldType.number, writable: false },
|
|
412
|
+
],
|
|
413
|
+
});
|
|
414
|
+
const impl = makeContract({ observables: [...contract.observables] });
|
|
415
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
416
|
+
expect(result.valid).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
it("fails when an observable is missing", () => {
|
|
419
|
+
const contract = makeContract({
|
|
420
|
+
observables: [
|
|
421
|
+
{ name: "taskCount", description: "d", valueType: field_type_1.FieldType.number, writable: false },
|
|
422
|
+
],
|
|
423
|
+
});
|
|
424
|
+
const impl = makeContract({ observables: [] });
|
|
425
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
426
|
+
expect(result.valid).toBe(false);
|
|
427
|
+
expect(result.errors[0].kind).toBe("observable");
|
|
428
|
+
});
|
|
429
|
+
it("fails on value type mismatch", () => {
|
|
430
|
+
const contract = makeContract({
|
|
431
|
+
observables: [
|
|
432
|
+
{ name: "taskCount", description: "d", valueType: field_type_1.FieldType.number, writable: false },
|
|
433
|
+
],
|
|
434
|
+
});
|
|
435
|
+
const impl = makeContract({
|
|
436
|
+
observables: [
|
|
437
|
+
{ name: "taskCount", description: "d", valueType: field_type_1.FieldType.string, writable: false },
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
441
|
+
expect(result.valid).toBe(false);
|
|
442
|
+
expect(result.errors[0].message).toContain("type mismatch");
|
|
443
|
+
});
|
|
444
|
+
it("fails when contract requires writable but impl is read-only", () => {
|
|
445
|
+
const contract = makeContract({
|
|
446
|
+
observables: [
|
|
447
|
+
{ name: "taskCount", description: "d", valueType: field_type_1.FieldType.number, writable: true },
|
|
448
|
+
],
|
|
449
|
+
});
|
|
450
|
+
const impl = makeContract({
|
|
451
|
+
observables: [
|
|
452
|
+
{ name: "taskCount", description: "d", valueType: field_type_1.FieldType.number, writable: false },
|
|
453
|
+
],
|
|
454
|
+
});
|
|
455
|
+
const result = (0, validate_1.validateProviderSatisfiesContract)(impl, contract);
|
|
456
|
+
expect(result.valid).toBe(false);
|
|
457
|
+
expect(result.errors[0].message).toContain("writable");
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
describe("validateImmutability", () => {
|
|
462
|
+
it("allows any change for dev contracts", () => {
|
|
463
|
+
const existing = makeContract({ devTag: "dev", tables: [] });
|
|
464
|
+
const incoming = makeContract({
|
|
465
|
+
devTag: "dev",
|
|
466
|
+
tables: [
|
|
467
|
+
{
|
|
468
|
+
name: "New",
|
|
469
|
+
description: "d",
|
|
470
|
+
primaryKeyName: "id",
|
|
471
|
+
fields: [{ name: "id", type: field_type_1.FieldType.id }],
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
});
|
|
475
|
+
const result = (0, validate_1.validateImmutability)(existing, incoming);
|
|
476
|
+
expect(result.valid).toBe(true);
|
|
477
|
+
});
|
|
478
|
+
it("rejects registering a dev version when stable version already exists", () => {
|
|
479
|
+
const existing = makeContract({
|
|
480
|
+
tables: [
|
|
481
|
+
{
|
|
482
|
+
name: "Tasks",
|
|
483
|
+
description: "d",
|
|
484
|
+
primaryKeyName: "taskId",
|
|
485
|
+
fields: [{ name: "taskId", type: field_type_1.FieldType.id }],
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
});
|
|
489
|
+
const incoming = makeContract({
|
|
490
|
+
devTag: "dev",
|
|
491
|
+
tables: [
|
|
492
|
+
{
|
|
493
|
+
name: "Tasks",
|
|
494
|
+
description: "d",
|
|
495
|
+
primaryKeyName: "taskId",
|
|
496
|
+
fields: [{ name: "taskId", type: field_type_1.FieldType.id }],
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
});
|
|
500
|
+
const result = (0, validate_1.validateImmutability)(existing, incoming);
|
|
501
|
+
expect(result.valid).toBe(false);
|
|
502
|
+
expect(result.errors[0].message).toContain("frozen");
|
|
503
|
+
});
|
|
504
|
+
it("passes when a frozen contract is re-published identically", () => {
|
|
505
|
+
const contract = makeContract({
|
|
506
|
+
tables: [
|
|
507
|
+
{
|
|
508
|
+
name: "Tasks",
|
|
509
|
+
description: "d",
|
|
510
|
+
primaryKeyName: "taskId",
|
|
511
|
+
fields: [{ name: "taskId", type: field_type_1.FieldType.id }],
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
});
|
|
515
|
+
const result = (0, validate_1.validateImmutability)(contract, { ...contract });
|
|
516
|
+
expect(result.valid).toBe(true);
|
|
517
|
+
});
|
|
518
|
+
it("fails when a frozen contract shape is modified", () => {
|
|
519
|
+
const existing = makeContract({
|
|
520
|
+
tables: [
|
|
521
|
+
{
|
|
522
|
+
name: "Tasks",
|
|
523
|
+
description: "d",
|
|
524
|
+
primaryKeyName: "taskId",
|
|
525
|
+
fields: [{ name: "taskId", type: field_type_1.FieldType.id }],
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
});
|
|
529
|
+
const incoming = makeContract({
|
|
530
|
+
tables: [
|
|
531
|
+
{
|
|
532
|
+
name: "Tasks",
|
|
533
|
+
description: "d",
|
|
534
|
+
primaryKeyName: "taskId",
|
|
535
|
+
fields: [
|
|
536
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
537
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
538
|
+
],
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
});
|
|
542
|
+
const result = (0, validate_1.validateImmutability)(existing, incoming);
|
|
543
|
+
expect(result.valid).toBe(false);
|
|
544
|
+
expect(result.errors[0].message).toContain("frozen");
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
describe("validateAlsoImplements", () => {
|
|
548
|
+
const contractStore = new Map();
|
|
549
|
+
const TASKS_CONTRACT_ID = (0, utils_1.newid)();
|
|
550
|
+
const FANCY_TASKS_CONTRACT_ID = (0, utils_1.newid)();
|
|
551
|
+
const NONEXISTENT_ID = (0, utils_1.newid)();
|
|
552
|
+
function getContract(id, ver) {
|
|
553
|
+
return contractStore.get((0, types_1.contractKey)(id, ver));
|
|
554
|
+
}
|
|
555
|
+
beforeEach(() => {
|
|
556
|
+
contractStore.clear();
|
|
557
|
+
});
|
|
558
|
+
it("passes when implementation is a superset of the target contract", () => {
|
|
559
|
+
const targetV1 = makeContract({
|
|
560
|
+
contractId: TASKS_CONTRACT_ID,
|
|
561
|
+
version: 1,
|
|
562
|
+
tables: [
|
|
563
|
+
{
|
|
564
|
+
name: "Tasks",
|
|
565
|
+
description: "d",
|
|
566
|
+
primaryKeyName: "taskId",
|
|
567
|
+
fields: [
|
|
568
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
569
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
570
|
+
],
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
});
|
|
574
|
+
contractStore.set((0, types_1.contractKey)(TASKS_CONTRACT_ID, 1), targetV1);
|
|
575
|
+
const impl = makeContract({
|
|
576
|
+
contractId: FANCY_TASKS_CONTRACT_ID,
|
|
577
|
+
version: 1,
|
|
578
|
+
tables: [
|
|
579
|
+
{
|
|
580
|
+
name: "Tasks",
|
|
581
|
+
description: "d",
|
|
582
|
+
primaryKeyName: "taskId",
|
|
583
|
+
fields: [
|
|
584
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
585
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
586
|
+
{ name: "priority", type: field_type_1.FieldType.number, optional: true },
|
|
587
|
+
],
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
});
|
|
591
|
+
const result = (0, validate_1.validateAlsoImplements)(impl, [{ contractId: TASKS_CONTRACT_ID, version: 1 }], getContract);
|
|
592
|
+
expect(result.valid).toBe(true);
|
|
593
|
+
});
|
|
594
|
+
it("validates all versions in a range", () => {
|
|
595
|
+
const v1 = makeContract({
|
|
596
|
+
contractId: TASKS_CONTRACT_ID,
|
|
597
|
+
version: 1,
|
|
598
|
+
tables: [
|
|
599
|
+
{
|
|
600
|
+
name: "Tasks",
|
|
601
|
+
description: "d",
|
|
602
|
+
primaryKeyName: "taskId",
|
|
603
|
+
fields: [
|
|
604
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
605
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
606
|
+
],
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
});
|
|
610
|
+
const v2 = makeContract({
|
|
611
|
+
contractId: TASKS_CONTRACT_ID,
|
|
612
|
+
version: 2,
|
|
613
|
+
tables: [
|
|
614
|
+
{
|
|
615
|
+
name: "Tasks",
|
|
616
|
+
description: "d",
|
|
617
|
+
primaryKeyName: "taskId",
|
|
618
|
+
fields: [
|
|
619
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
620
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
621
|
+
{ name: "status", type: field_type_1.FieldType.string, optional: true },
|
|
622
|
+
],
|
|
623
|
+
},
|
|
624
|
+
],
|
|
625
|
+
});
|
|
626
|
+
contractStore.set((0, types_1.contractKey)(TASKS_CONTRACT_ID, 1), v1);
|
|
627
|
+
contractStore.set((0, types_1.contractKey)(TASKS_CONTRACT_ID, 2), v2);
|
|
628
|
+
// Implementation satisfies both: has all fields from both versions.
|
|
629
|
+
// 'status' is optional so v1 consumers can still write valid rows.
|
|
630
|
+
const impl = makeContract({
|
|
631
|
+
tables: [
|
|
632
|
+
{
|
|
633
|
+
name: "Tasks",
|
|
634
|
+
description: "d",
|
|
635
|
+
primaryKeyName: "taskId",
|
|
636
|
+
fields: [
|
|
637
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
638
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
639
|
+
{ name: "status", type: field_type_1.FieldType.string, optional: true },
|
|
640
|
+
],
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
});
|
|
644
|
+
const result = (0, validate_1.validateAlsoImplements)(impl, [{ contractId: TASKS_CONTRACT_ID, version: { from: 1, to: 2 } }], getContract);
|
|
645
|
+
expect(result.valid).toBe(true);
|
|
646
|
+
});
|
|
647
|
+
it("fails when implementation is missing a field from one version in range", () => {
|
|
648
|
+
const v1 = makeContract({
|
|
649
|
+
contractId: TASKS_CONTRACT_ID,
|
|
650
|
+
version: 1,
|
|
651
|
+
tables: [
|
|
652
|
+
{
|
|
653
|
+
name: "Tasks",
|
|
654
|
+
description: "d",
|
|
655
|
+
primaryKeyName: "taskId",
|
|
656
|
+
fields: [{ name: "taskId", type: field_type_1.FieldType.id }],
|
|
657
|
+
},
|
|
658
|
+
],
|
|
659
|
+
});
|
|
660
|
+
const v2 = makeContract({
|
|
661
|
+
contractId: TASKS_CONTRACT_ID,
|
|
662
|
+
version: 2,
|
|
663
|
+
tables: [
|
|
664
|
+
{
|
|
665
|
+
name: "Tasks",
|
|
666
|
+
description: "d",
|
|
667
|
+
primaryKeyName: "taskId",
|
|
668
|
+
fields: [
|
|
669
|
+
{ name: "taskId", type: field_type_1.FieldType.id },
|
|
670
|
+
{ name: "assignee", type: field_type_1.FieldType.string },
|
|
671
|
+
],
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
});
|
|
675
|
+
contractStore.set((0, types_1.contractKey)(TASKS_CONTRACT_ID, 1), v1);
|
|
676
|
+
contractStore.set((0, types_1.contractKey)(TASKS_CONTRACT_ID, 2), v2);
|
|
677
|
+
// Implementation only satisfies v1 (missing 'assignee')
|
|
678
|
+
const impl = makeContract({
|
|
679
|
+
tables: [
|
|
680
|
+
{
|
|
681
|
+
name: "Tasks",
|
|
682
|
+
description: "d",
|
|
683
|
+
primaryKeyName: "taskId",
|
|
684
|
+
fields: [{ name: "taskId", type: field_type_1.FieldType.id }],
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
});
|
|
688
|
+
const result = (0, validate_1.validateAlsoImplements)(impl, [{ contractId: TASKS_CONTRACT_ID, version: { from: 1, to: 2 } }], getContract);
|
|
689
|
+
expect(result.valid).toBe(false);
|
|
690
|
+
expect(result.errors[0].message).toContain("v2");
|
|
691
|
+
expect(result.errors[0].message).toContain("assignee");
|
|
692
|
+
});
|
|
693
|
+
it("fails when a referenced contract does not exist", () => {
|
|
694
|
+
const impl = makeContract({ tables: [] });
|
|
695
|
+
const result = (0, validate_1.validateAlsoImplements)(impl, [{ contractId: NONEXISTENT_ID, version: 1 }], getContract);
|
|
696
|
+
expect(result.valid).toBe(false);
|
|
697
|
+
expect(result.errors[0].message).toContain("not found");
|
|
698
|
+
});
|
|
699
|
+
});
|