@jarenjs/contract 0.49.2 → 0.56.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/README.md +57 -10
- package/dist/types/project/tools.d.ts +1 -1
- package/dist/types/project/typescript.d.ts +11 -0
- package/docs/CONTRACT-FORMAT.md +37 -17
- package/package.json +5 -5
- package/src/project/tools.js +9 -2
- package/src/project/typescript.js +91 -1
package/README.md
CHANGED
|
@@ -51,8 +51,9 @@ published as JSON Schema in
|
|
|
51
51
|
"$contract": "0.1",
|
|
52
52
|
"id": "shop",
|
|
53
53
|
"$defs": {
|
|
54
|
-
"Product": { "type": "object",
|
|
55
|
-
"properties": { "id": { "type": "integer" }, "name": { "type": "string" } }
|
|
54
|
+
"Product": { "type": "object",
|
|
55
|
+
"properties": { "id": { "type": "integer" }, "name": { "type": "string" } },
|
|
56
|
+
"required": ["id", "name"] }
|
|
56
57
|
},
|
|
57
58
|
"operations": {
|
|
58
59
|
"catalog.load": {
|
|
@@ -64,9 +65,10 @@ published as JSON Schema in
|
|
|
64
65
|
},
|
|
65
66
|
"product.save": {
|
|
66
67
|
"kind": "command",
|
|
67
|
-
"input": { "type": "object", "
|
|
68
|
+
"input": { "type": "object", "properties": {
|
|
68
69
|
"id": { "type": "integer" }, "revision": { "type": "integer" },
|
|
69
|
-
"product": { "$ref": "#/$defs/Product" } }
|
|
70
|
+
"product": { "$ref": "#/$defs/Product" } },
|
|
71
|
+
"required": ["id", "revision", "product"] },
|
|
70
72
|
"output": { "$ref": "#/$defs/Product" },
|
|
71
73
|
"errors": { "conflict": { "status": 409 }, "not-found": { "status": 404 } },
|
|
72
74
|
"policy": { "idempotency": "required", "revision": "input:/revision" },
|
|
@@ -74,7 +76,7 @@ published as JSON Schema in
|
|
|
74
76
|
},
|
|
75
77
|
"image.bytes": {
|
|
76
78
|
"kind": "read",
|
|
77
|
-
"input": { "type": "object", "
|
|
79
|
+
"input": { "type": "object", "properties": { "id": { "type": "integer" } }, "required": ["id"] },
|
|
78
80
|
"output": true,
|
|
79
81
|
"http": { "method": "GET", "path": "/api/images/{id}", "media": "application/octet-stream" }
|
|
80
82
|
}
|
|
@@ -90,6 +92,51 @@ members are never coerced. An operation without `http` is bound to the
|
|
|
90
92
|
canonical `POST /<op-id>`. A non-JSON `media` marks an operation
|
|
91
93
|
*opaque*: routed and matched, never validated as JSON.
|
|
92
94
|
|
|
95
|
+
## The same document, by code
|
|
96
|
+
|
|
97
|
+
`@jarenjs/linq/contract` is the pen that writes this format. The
|
|
98
|
+
builders are the schema pen's, every `named()` schema is hoisted into
|
|
99
|
+
the contract's `$defs`, the members land in the order §12.1 fixes, and
|
|
100
|
+
no default is written — so the document below is byte for byte the one
|
|
101
|
+
above:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import * as s from '@jarenjs/linq/schema';
|
|
105
|
+
import { command, defineContract, error, http, read } from '@jarenjs/linq/contract';
|
|
106
|
+
|
|
107
|
+
const Product = s.named('Product', s.object({ id: s.integer(), name: s.string() }).open());
|
|
108
|
+
|
|
109
|
+
export const shop = defineContract({ id: 'shop' }, {
|
|
110
|
+
'catalog.load': read({
|
|
111
|
+
input: s.object({ since: s.string().format('date-time').optional() }).open(),
|
|
112
|
+
output: s.array(Product),
|
|
113
|
+
policy: { task: 'switch', cache: 'revision' },
|
|
114
|
+
http: http({ method: 'GET', path: '/api/catalog' }),
|
|
115
|
+
}),
|
|
116
|
+
'product.save': command({
|
|
117
|
+
input: s.object({ id: s.integer(), revision: s.integer(), product: Product }).open(),
|
|
118
|
+
output: Product,
|
|
119
|
+
errors: { conflict: error({ status: 409 }), 'not-found': error({ status: 404 }) },
|
|
120
|
+
policy: { idempotency: 'required', revision: 'input:/revision' },
|
|
121
|
+
http: http({ method: 'PUT', path: '/api/products/{id}/master' }),
|
|
122
|
+
}),
|
|
123
|
+
'image.bytes': read({
|
|
124
|
+
input: s.object({ id: s.integer() }).open(),
|
|
125
|
+
output: true,
|
|
126
|
+
http: http({ method: 'GET', path: '/api/images/{id}', media: 'application/octet-stream' }),
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
compileContract(shop.document); // the same compile, the same errors
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The types come with it, without the `types` projection: `ContractOf<typeof
|
|
134
|
+
shop>` is the operation map, and `typedClient`, `typedHandlers` and
|
|
135
|
+
`typedTools` carry it onto a client, a handler table and an AI toolbox.
|
|
136
|
+
The pen's document is
|
|
137
|
+
[CONTRACT-PEN.md](../linq/docs/CONTRACT-PEN.md); it imports nothing of
|
|
138
|
+
this package.
|
|
139
|
+
|
|
93
140
|
## Compile once, use everywhere
|
|
94
141
|
|
|
95
142
|
```js
|
|
@@ -492,19 +539,19 @@ benchmark-figure gate so no number here is typed by hand:
|
|
|
492
539
|
|
|
493
540
|
- **Route match**: the compiled matcher resolves the probe mix —
|
|
494
541
|
static hot paths, variables, the static-beats-variable case, a miss —
|
|
495
|
-
at <!--
|
|
496
|
-
hono's TrieRouter is <!--
|
|
542
|
+
at <!--fact:contract.match.vs-fmw-->172 ns per lookup vs find-my-way's 180 ns<!--/fact-->;
|
|
543
|
+
hono's TrieRouter is <!--fact:contract.match.vs-hono-->1.8x<!--/fact--> behind, and its RegExpRouter refuses this
|
|
497
544
|
route table outright (a static path registered after a param sibling).
|
|
498
545
|
- **Dispatch, in-process**: the whole pipeline (route, decode, validate
|
|
499
546
|
input, handler, validate output,
|
|
500
|
-
serialize) is <!--
|
|
547
|
+
serialize) is <!--fact:contract.dispatch.vs-fastify-->2.8–15.1x<!--/fact-->
|
|
501
548
|
faster than Fastify driven through its own `inject` — a number that
|
|
502
549
|
includes Fastify's mock-stream harness, which is why the next row
|
|
503
550
|
exists.
|
|
504
551
|
- **The honest loss**: the bare pieces Fastify composes — find-my-way +
|
|
505
552
|
Ajv + fast-json-stringify, called directly with no harness and no
|
|
506
553
|
response validation
|
|
507
|
-
— are <!--
|
|
554
|
+
— are <!--fact:contract.dispatch.losses-->2.4–7.5x<!--/fact--> faster than
|
|
508
555
|
this pipeline. The wide end of that band is the bare `{ok:true}`
|
|
509
556
|
route, where the rival's compiled serializer answers in ~200 ns and
|
|
510
557
|
there is almost no work to amortize the pipeline against; on the
|
|
@@ -514,7 +561,7 @@ benchmark-figure gate so no number here is typed by hand:
|
|
|
514
561
|
server kept its own contract before a byte leaves. Over a real
|
|
515
562
|
loopback socket the two stacks are level: the socket dominates both.
|
|
516
563
|
- **Revision**: computing it
|
|
517
|
-
costs <!--
|
|
564
|
+
costs <!--fact:contract.revision.ms-->1.4 ms<!--/fact--> for the 123-operation
|
|
518
565
|
contract, once per process.
|
|
519
566
|
|
|
520
567
|
## What it is not
|
|
@@ -22,6 +22,17 @@
|
|
|
22
22
|
* stylesheet — the D6 shapes as every binding carries them
|
|
23
23
|
* (`OUTCOME_META_MEMBERS` / `OUTCOME_ERROR_MEMBERS` in the client module
|
|
24
24
|
* are the runtime twins; a test holds the text to them).
|
|
25
|
+
*
|
|
26
|
+
* One convention rides on top of emit's reading, the suite's: a string
|
|
27
|
+
* with `format: "date-time"` or `format: "date"` is the `DateTime`
|
|
28
|
+
* brand, so a consumer's generated types agree with `@jarenjs/db`'s
|
|
29
|
+
* entity types (`entityEmitModel`) and `@jarenjs/linq`'s schema pen,
|
|
30
|
+
* which both read a date format that way. Emit itself records a format
|
|
31
|
+
* only as a dropped constraint, so the brand is applied HERE, by
|
|
32
|
+
* rewriting date-formatted string nodes to a `$ref` of one shared
|
|
33
|
+
* definition before emit reads the document — in every position, an
|
|
34
|
+
* array item as much as a member — and giving that definition the brand
|
|
35
|
+
* intersection.
|
|
25
36
|
*/
|
|
26
37
|
export type Contract = import('../compile.js').Contract;
|
|
27
38
|
export type CompiledOperation = import('../compile.js').CompiledOperation;
|
package/docs/CONTRACT-FORMAT.md
CHANGED
|
@@ -17,6 +17,15 @@ declared errors, a behavior policy and an HTTP binding. It is compiled
|
|
|
17
17
|
path matcher, and it is the single source every artifact around it is
|
|
18
18
|
projected from.
|
|
19
19
|
|
|
20
|
+
A contract document is written by hand, or by code: `@jarenjs/linq/
|
|
21
|
+
contract` is the pen that writes exactly this format — the same
|
|
22
|
+
operations, schemas, policies and bindings, in §12.1's member order,
|
|
23
|
+
with the operations' named schemas hoisted into `$defs` — and it types
|
|
24
|
+
the client, the handler table and the AI tools from the same builders,
|
|
25
|
+
without running the TypeScript projection of §12.3. The three worked
|
|
26
|
+
examples below are rebuilt through it, byte for byte, by its own test
|
|
27
|
+
suite.
|
|
28
|
+
|
|
20
29
|
Format 0.1 covers the document, its compilation, the HTTP binding's
|
|
21
30
|
*shape* (§2–§6), the HTTP **server** binding that carries it (§7–§9:
|
|
22
31
|
the request pipeline and its wire errors, idempotency and the ledger
|
|
@@ -48,22 +57,22 @@ coming lines of this package and will append their sections here.
|
|
|
48
57
|
"version": "5",
|
|
49
58
|
"compat": ["4"],
|
|
50
59
|
"$defs": {
|
|
60
|
+
"Catalog": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"revision": { "type": "integer" },
|
|
64
|
+
"products": { "type": "array", "items": { "$ref": "#/$defs/Product" } }
|
|
65
|
+
},
|
|
66
|
+
"required": ["revision", "products"]
|
|
67
|
+
},
|
|
51
68
|
"Product": {
|
|
52
69
|
"type": "object",
|
|
53
|
-
"required": ["id", "name", "price"],
|
|
54
70
|
"properties": {
|
|
55
71
|
"id": { "type": "integer" },
|
|
56
72
|
"name": { "type": "string", "minLength": 1 },
|
|
57
73
|
"price": { "type": "number", "minimum": 0 }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
"Catalog": {
|
|
61
|
-
"type": "object",
|
|
62
|
-
"required": ["revision", "products"],
|
|
63
|
-
"properties": {
|
|
64
|
-
"revision": { "type": "integer" },
|
|
65
|
-
"products": { "type": "array", "items": { "$ref": "#/$defs/Product" } }
|
|
66
|
-
}
|
|
74
|
+
},
|
|
75
|
+
"required": ["id", "name", "price"]
|
|
67
76
|
},
|
|
68
77
|
"Conflict": { "type": "object", "properties": { "current": { "$ref": "#/$defs/Product" } } }
|
|
69
78
|
},
|
|
@@ -81,12 +90,12 @@ coming lines of this package and will append their sections here.
|
|
|
81
90
|
"kind": "command",
|
|
82
91
|
"input": {
|
|
83
92
|
"type": "object",
|
|
84
|
-
"required": ["id", "revision", "product"],
|
|
85
93
|
"properties": {
|
|
86
94
|
"id": { "type": "integer" },
|
|
87
95
|
"revision": { "type": "integer" },
|
|
88
96
|
"product": { "$ref": "#/$defs/Product" }
|
|
89
|
-
}
|
|
97
|
+
},
|
|
98
|
+
"required": ["id", "revision", "product"]
|
|
90
99
|
},
|
|
91
100
|
"output": { "$ref": "#/$defs/Product" },
|
|
92
101
|
"errors": {
|
|
@@ -102,7 +111,7 @@ coming lines of this package and will append their sections here.
|
|
|
102
111
|
},
|
|
103
112
|
"image.bytes": {
|
|
104
113
|
"kind": "read",
|
|
105
|
-
"input": { "type": "object", "
|
|
114
|
+
"input": { "type": "object", "properties": { "id": { "type": "integer" } }, "required": ["id"] },
|
|
106
115
|
"output": true,
|
|
107
116
|
"http": { "method": "GET", "path": "/api/images/{id}", "media": "application/octet-stream" }
|
|
108
117
|
}
|
|
@@ -427,20 +436,20 @@ The three worked examples this document is tested against, complete:
|
|
|
427
436
|
"kind": "command",
|
|
428
437
|
"input": {
|
|
429
438
|
"type": "object",
|
|
430
|
-
"required": ["id", "doc"],
|
|
431
439
|
"properties": {
|
|
432
440
|
"id": { "type": "string" },
|
|
433
441
|
"doc": { "type": "array", "items": { "type": "object" } },
|
|
434
442
|
"dry": { "type": "boolean" }
|
|
435
|
-
}
|
|
443
|
+
},
|
|
444
|
+
"required": ["id", "doc"]
|
|
436
445
|
},
|
|
437
446
|
"output": true,
|
|
438
447
|
"policy": { "idempotency": "optional" },
|
|
439
|
-
"http": { "method": "PUT", "path": "/docs/:id", "
|
|
448
|
+
"http": { "method": "PUT", "path": "/docs/:id", "in": { "dry": "query" }, "body": "doc", "status": 204 }
|
|
440
449
|
},
|
|
441
450
|
"doc.remove": {
|
|
442
451
|
"kind": "command",
|
|
443
|
-
"input": { "type": "object", "
|
|
452
|
+
"input": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] },
|
|
444
453
|
"output": true
|
|
445
454
|
}
|
|
446
455
|
}
|
|
@@ -1555,6 +1564,17 @@ a test holds the text to them; `details` is `unknown` and `status`
|
|
|
1555
1564
|
`number | null`, never optional members. An input-less operation's
|
|
1556
1565
|
`input` is `null`; an opaque operation appears only in `UrlOperations`.
|
|
1557
1566
|
|
|
1567
|
+
One convention rides on top of emit's reading, and it is the suite's:
|
|
1568
|
+
a string with `format: "date-time"` or `format: "date"` is declared as
|
|
1569
|
+
`DateTime` — `string & { __jarenTag: 'date-time' }` — rendered once per
|
|
1570
|
+
document and referenced from every position, an array item as much as a
|
|
1571
|
+
member. Emit itself records a format only as a dropped constraint; the
|
|
1572
|
+
brand is applied by this projection so a consumer's generated types
|
|
1573
|
+
agree with `@jarenjs/db`'s entity types (`entityEmitModel`) and with
|
|
1574
|
+
`@jarenjs/linq`'s schema and contract pens, which read a date format the
|
|
1575
|
+
same way. A contract that already declares a `$defs` entry named
|
|
1576
|
+
`DateTime` keeps it; the brand takes the next free name.
|
|
1577
|
+
|
|
1558
1578
|
### §12.4 Markdown
|
|
1559
1579
|
|
|
1560
1580
|
`toMarkdown(contract, { title? })` renders one reference document: the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/contract",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.56.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -102,9 +102,9 @@
|
|
|
102
102
|
"prepack": "npm run build:types"
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
105
|
-
"@jarenjs/core": "^0.
|
|
106
|
-
"@jarenjs/json": "^0.
|
|
107
|
-
"@jarenjs/validate": "^0.
|
|
108
|
-
"@jarenjs/emit": "^0.
|
|
105
|
+
"@jarenjs/core": "^0.56.0",
|
|
106
|
+
"@jarenjs/json": "^0.56.0",
|
|
107
|
+
"@jarenjs/validate": "^0.56.0",
|
|
108
|
+
"@jarenjs/emit": "^0.56.0"
|
|
109
109
|
}
|
|
110
110
|
}
|
package/src/project/tools.js
CHANGED
|
@@ -35,8 +35,15 @@ import { bundleSameDocument } from '../bundle.js';
|
|
|
35
35
|
/**
|
|
36
36
|
* A client as the tools read it — any binding's client. The members are
|
|
37
37
|
* `any` so a client whose `invoke` narrows its own context type (the http
|
|
38
|
-
* client's `InvokeContext`) still assigns
|
|
39
|
-
*
|
|
38
|
+
* client's `InvokeContext`) still assigns, and `invoke` is declared as a
|
|
39
|
+
* METHOD rather than as a function-valued property: a method's parameters
|
|
40
|
+
* are bivariant, so a client whose `invoke` narrows its OPERATION type to
|
|
41
|
+
* a literal union — `@jarenjs/linq/contract`'s `typedClient`, whose whole
|
|
42
|
+
* purpose is that narrowing — assigns here too. Under
|
|
43
|
+
* `strictFunctionTypes` the property form rejects it, and there is
|
|
44
|
+
* nothing to reject: this module only ever CALLS `invoke`, with an id it
|
|
45
|
+
* read out of the contract the client was opened on.
|
|
46
|
+
* @typedef {{ invoke(op: string, input: any, ctx?: any): any }} ToolClient
|
|
40
47
|
*/
|
|
41
48
|
|
|
42
49
|
/**
|
|
@@ -23,6 +23,17 @@
|
|
|
23
23
|
* stylesheet — the D6 shapes as every binding carries them
|
|
24
24
|
* (`OUTCOME_META_MEMBERS` / `OUTCOME_ERROR_MEMBERS` in the client module
|
|
25
25
|
* are the runtime twins; a test holds the text to them).
|
|
26
|
+
*
|
|
27
|
+
* One convention rides on top of emit's reading, the suite's: a string
|
|
28
|
+
* with `format: "date-time"` or `format: "date"` is the `DateTime`
|
|
29
|
+
* brand, so a consumer's generated types agree with `@jarenjs/db`'s
|
|
30
|
+
* entity types (`entityEmitModel`) and `@jarenjs/linq`'s schema pen,
|
|
31
|
+
* which both read a date format that way. Emit itself records a format
|
|
32
|
+
* only as a dropped constraint, so the brand is applied HERE, by
|
|
33
|
+
* rewriting date-formatted string nodes to a `$ref` of one shared
|
|
34
|
+
* definition before emit reads the document — in every position, an
|
|
35
|
+
* array item as much as a member — and giving that definition the brand
|
|
36
|
+
* intersection.
|
|
26
37
|
*/
|
|
27
38
|
|
|
28
39
|
import { isJsonObject, setObjectMember } from '@jarenjs/core/object';
|
|
@@ -66,6 +77,72 @@ function pascal(word) {
|
|
|
66
77
|
return out;
|
|
67
78
|
}
|
|
68
79
|
|
|
80
|
+
/** The two formats that carry the brand. */
|
|
81
|
+
const DATE_FORMATS = ['date-time', 'date'];
|
|
82
|
+
|
|
83
|
+
/** The definition name the brand takes when the contract leaves it free. */
|
|
84
|
+
const DATE_TIME = 'DateTime';
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Rewrite every date-formatted string node to a reference to the shared
|
|
88
|
+
* brand definition, in every position. Nodes are rebuilt, never mutated:
|
|
89
|
+
* the schemas here are the compiled document's own frozen subtrees.
|
|
90
|
+
* @param {any} node
|
|
91
|
+
* @param {string} name - the brand definition's name in this document
|
|
92
|
+
* @returns {any}
|
|
93
|
+
*/
|
|
94
|
+
function brandDates(node, name) {
|
|
95
|
+
if (Array.isArray(node)) return node.map((item) => brandDates(item, name));
|
|
96
|
+
if (node === null || typeof node !== 'object') return node;
|
|
97
|
+
/** @type {Record<string, any>} */
|
|
98
|
+
const out = {};
|
|
99
|
+
const keys = Object.keys(node);
|
|
100
|
+
for (let i = 0; i < keys.length; i++) setObjectMember(out, keys[i], brandDates(node[keys[i]], name));
|
|
101
|
+
if (!DATE_FORMATS.includes(/** @type {any} */ (out.format))) return out;
|
|
102
|
+
const { type, format: _format, ...rest } = out;
|
|
103
|
+
if (type === 'string') return { $ref: `#/$defs/${name}`, ...rest };
|
|
104
|
+
// a nullable date: the brand or null, the rest of the node kept
|
|
105
|
+
if (Array.isArray(type) && type.length === 2 && type.includes('string') && type.includes('null')) {
|
|
106
|
+
return { anyOf: [{ $ref: `#/$defs/${name}` }, { type: 'null' }], ...rest };
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Whether a document reaches a date-formatted string anywhere. */
|
|
112
|
+
function usesDates(node) {
|
|
113
|
+
if (Array.isArray(node)) return node.some(usesDates);
|
|
114
|
+
if (node === null || typeof node !== 'object') return false;
|
|
115
|
+
if (DATE_FORMATS.includes(node.format)) return true;
|
|
116
|
+
return Object.values(node).some(usesDates);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Replace the brand definition's compiled declaration — a plain
|
|
121
|
+
* `string` — with the brand intersection `string & { __jarenTag:
|
|
122
|
+
* 'date-time' }`, structurally identical to `@jarenjs/db`'s and
|
|
123
|
+
* `@jarenjs/linq`'s.
|
|
124
|
+
* @param {any} model
|
|
125
|
+
* @param {string} name
|
|
126
|
+
*/
|
|
127
|
+
function brandDeclaration(model, name) {
|
|
128
|
+
const declaration = model.declarations.find((/** @type {any} */ d) => d.name === name);
|
|
129
|
+
/* c8 ignore next -- the definition is added exactly when it is referenced */
|
|
130
|
+
if (declaration === undefined) return;
|
|
131
|
+
declaration.type = {
|
|
132
|
+
kind: 'intersection',
|
|
133
|
+
parts: [{ kind: 'primitive', primitive: 'string' }, {
|
|
134
|
+
kind: 'object',
|
|
135
|
+
members: [{
|
|
136
|
+
kind: 'member', name: '__jarenTag',
|
|
137
|
+
type: { kind: 'literal', value: 'date-time' }, required: true,
|
|
138
|
+
constraints: [], doc: [],
|
|
139
|
+
}],
|
|
140
|
+
}],
|
|
141
|
+
};
|
|
142
|
+
declaration.doc = ['An RFC 3339 string branded for the date operators;',
|
|
143
|
+
'structurally identical to the @jarenjs/linq and @jarenjs/db brand.'];
|
|
144
|
+
}
|
|
145
|
+
|
|
69
146
|
/**
|
|
70
147
|
* The type model of a contract: the synthetic `$defs` root, the row per
|
|
71
148
|
* operation the stylesheet renders, and the rendered declarations.
|
|
@@ -117,7 +194,20 @@ export function contractTypeModel(contract, ops, source) {
|
|
|
117
194
|
}
|
|
118
195
|
const names = Object.keys(contractDefs);
|
|
119
196
|
for (let i = 0; i < names.length; i++) setObjectMember(defs, names[i], contractDefs[names[i]]);
|
|
120
|
-
|
|
197
|
+
/** @type {Record<string, any>} */
|
|
198
|
+
let root = defs;
|
|
199
|
+
/** @type {string | null} */
|
|
200
|
+
let brand = null;
|
|
201
|
+
if (usesDates(defs)) {
|
|
202
|
+
// the brand takes its own name unless the contract already spells it
|
|
203
|
+
brand = taken.has(DATE_TIME) ? unique(DATE_TIME) : DATE_TIME;
|
|
204
|
+
root = { [brand]: { type: 'string' } };
|
|
205
|
+
const branded = brandDates(defs, brand);
|
|
206
|
+
const keys = Object.keys(branded);
|
|
207
|
+
for (let i = 0; i < keys.length; i++) setObjectMember(root, keys[i], branded[keys[i]]);
|
|
208
|
+
}
|
|
209
|
+
const model = compileEmitModel({ $defs: root }, { name: 'Contract', source });
|
|
210
|
+
if (brand !== null) brandDeclaration(model, brand);
|
|
121
211
|
const declarations = model.declarations.filter((d) => d.name !== model.root);
|
|
122
212
|
const types = renderTypeScript({ ...model, declarations }, { banner: false });
|
|
123
213
|
return { types, rows, model: { ...model, declarations } };
|