@highstate/contract 0.19.1 → 0.20.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/index.js +682 -426
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/component.spec.ts +912 -3
- package/src/component.ts +524 -89
- package/src/cuidv2d.spec.ts +39 -0
- package/src/cuidv2d.ts +54 -0
- package/src/entity.ts +321 -81
- package/src/evaluation.ts +62 -12
- package/src/index.ts +9 -1
- package/src/instance-input.ts +219 -0
- package/src/instance.ts +307 -65
- package/src/json-schema.ts +115 -0
- package/src/pulumi.ts +45 -16
- package/src/unit.ts +26 -10
- package/src/compaction.spec.ts +0 -215
- package/src/compaction.ts +0 -381
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { cuidv2d } from "./cuidv2d"
|
|
3
|
+
|
|
4
|
+
describe("cuidv2d", () => {
|
|
5
|
+
it("is deterministic for the same inputs", () => {
|
|
6
|
+
const id1 = cuidv2d("entityType", "identity")
|
|
7
|
+
const id2 = cuidv2d("entityType", "identity")
|
|
8
|
+
|
|
9
|
+
expect(id1).toBe(id2)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it("changes when identity changes", () => {
|
|
13
|
+
const id1 = cuidv2d("entityType", "identity1")
|
|
14
|
+
const id2 = cuidv2d("entityType", "identity2")
|
|
15
|
+
|
|
16
|
+
expect(id1).not.toBe(id2)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("changes when namespace changes", () => {
|
|
20
|
+
const id1 = cuidv2d("entityType1", "identity")
|
|
21
|
+
const id2 = cuidv2d("entityType2", "identity")
|
|
22
|
+
|
|
23
|
+
expect(id1).not.toBe(id2)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("uses the CUIDv2 character set and length", () => {
|
|
27
|
+
const id = cuidv2d("entityType", "identity")
|
|
28
|
+
|
|
29
|
+
expect(id).toMatch(/^[a-z][0-9a-z]{23}$/)
|
|
30
|
+
expect(id).toHaveLength(24)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("does not collide for ambiguous concatenations", () => {
|
|
34
|
+
const a = cuidv2d("ab", "c")
|
|
35
|
+
const b = cuidv2d("a", "bc")
|
|
36
|
+
|
|
37
|
+
expect(a).not.toBe(b)
|
|
38
|
+
})
|
|
39
|
+
})
|
package/src/cuidv2d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2.js"
|
|
2
|
+
|
|
3
|
+
const cuidv2DefaultLength = 24
|
|
4
|
+
|
|
5
|
+
function bufToBigInt(buf: Uint8Array): bigint {
|
|
6
|
+
const bits = 8n
|
|
7
|
+
|
|
8
|
+
let value = 0n
|
|
9
|
+
for (const byte of buf.values()) {
|
|
10
|
+
value = (value << bits) + BigInt(byte)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hashCuid2(input: string): string {
|
|
17
|
+
const bytes = new TextEncoder().encode(input)
|
|
18
|
+
const digest = sha256(bytes)
|
|
19
|
+
|
|
20
|
+
return bufToBigInt(digest).toString(36).slice(1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeCuid2Prefix(prefix: string): string {
|
|
24
|
+
if (prefix >= "a" && prefix <= "z") {
|
|
25
|
+
return prefix
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (prefix >= "0" && prefix <= "9") {
|
|
29
|
+
const digit = prefix.charCodeAt(0) - "0".charCodeAt(0)
|
|
30
|
+
return String.fromCharCode("a".charCodeAt(0) + digit)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new Error(`Invalid CUID prefix character: ${prefix}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generates a CUIDv2d string from the given namespace and identity.
|
|
38
|
+
*
|
|
39
|
+
* CUIDv2d is a Highstate-specific ID format that generates a deterministic ID in the same format as CUIDv2.
|
|
40
|
+
*
|
|
41
|
+
* It uses the same encoding as CUIDv2, but instead of randomly generated values, it generates the ID based on the given namespace and identity.
|
|
42
|
+
*
|
|
43
|
+
* The generated ID is always 24 characters long.
|
|
44
|
+
* The first character is guaranteed to be a letter (`a-z`) to match CUIDv2 expectations.
|
|
45
|
+
* If the hash-derived first character would be a digit (`0-9`), it is deterministically mapped to a letter (`a-j`).
|
|
46
|
+
*/
|
|
47
|
+
export function cuidv2d(namespace: string, identity: string): string {
|
|
48
|
+
const hashInput = `${namespace}:${identity}`
|
|
49
|
+
const hashed = hashCuid2(hashInput)
|
|
50
|
+
const raw = hashed.slice(0, cuidv2DefaultLength)
|
|
51
|
+
const prefix = normalizeCuid2Prefix(raw[0]!)
|
|
52
|
+
|
|
53
|
+
return `${prefix}${raw.slice(1)}`
|
|
54
|
+
}
|
package/src/entity.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { IsEmptyObject, Simplify } from "type-fest"
|
|
2
2
|
import type { PartialKeys } from "./utils"
|
|
3
3
|
import { z } from "zod"
|
|
4
|
+
import { cuidv2d } from "./cuidv2d"
|
|
4
5
|
import { camelCaseToHumanReadable } from "./i18n"
|
|
6
|
+
import { fixJsonSchema } from "./json-schema"
|
|
5
7
|
import {
|
|
6
8
|
objectMetaSchema,
|
|
7
9
|
parseVersionedName,
|
|
@@ -90,11 +92,17 @@ export type EntityModel = z.infer<typeof entityModelSchema>
|
|
|
90
92
|
export type EntityTypes = Record<VersionedName, true>
|
|
91
93
|
|
|
92
94
|
export declare const implementedTypes: unique symbol
|
|
95
|
+
export declare const entityOutput: unique symbol
|
|
96
|
+
export declare const entityInput: unique symbol
|
|
97
|
+
export declare const entityIncludes: unique symbol
|
|
93
98
|
|
|
94
99
|
export type Entity<
|
|
95
100
|
TType extends VersionedName = VersionedName,
|
|
96
101
|
TImplementedTypes extends EntityTypes = EntityTypes,
|
|
97
102
|
TSchema extends z.ZodTypeAny = z.ZodTypeAny,
|
|
103
|
+
TOutput = z.output<TSchema>,
|
|
104
|
+
TInput = z.input<TSchema>,
|
|
105
|
+
TIncludes extends Record<string, unknown> = Record<string, unknown>,
|
|
98
106
|
> = {
|
|
99
107
|
/**
|
|
100
108
|
* The static type of the entity.
|
|
@@ -113,12 +121,128 @@ export type Entity<
|
|
|
113
121
|
*/
|
|
114
122
|
schema: TSchema
|
|
115
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The inferred value type of the entity.
|
|
126
|
+
*
|
|
127
|
+
* Does not exist at runtime.
|
|
128
|
+
*/
|
|
129
|
+
[entityOutput]: TOutput
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The inferred input type of the entity.
|
|
133
|
+
*
|
|
134
|
+
* Does not exist at runtime.
|
|
135
|
+
*/
|
|
136
|
+
[entityInput]: TInput
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The record of entity inclusions defined in `defineEntity({ includes: ... })`.
|
|
140
|
+
*
|
|
141
|
+
* Does not exist at runtime.
|
|
142
|
+
*/
|
|
143
|
+
[entityIncludes]: TIncludes
|
|
144
|
+
|
|
116
145
|
/**
|
|
117
146
|
* The model of the entity.
|
|
118
147
|
*/
|
|
119
148
|
model: EntityModel
|
|
120
149
|
}
|
|
121
150
|
|
|
151
|
+
export type EntityValue<TEntity> = TEntity extends { [entityOutput]: infer TValue } ? TValue : never
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The input type accepted by the entity schema.
|
|
155
|
+
*/
|
|
156
|
+
export type EntityValueInput<TEntity> = TEntity extends { [entityInput]: infer TValue }
|
|
157
|
+
? TValue
|
|
158
|
+
: never
|
|
159
|
+
|
|
160
|
+
type DistributiveSimplify<T> = T extends unknown ? Simplify<T> : never
|
|
161
|
+
|
|
162
|
+
type ResolveIncludeEntity<TIncludeRef> = TIncludeRef extends { entity: infer E }
|
|
163
|
+
? E extends () => infer ER
|
|
164
|
+
? ER
|
|
165
|
+
: E
|
|
166
|
+
: TIncludeRef
|
|
167
|
+
|
|
168
|
+
type IsIncludeMultiple<TIncludeRef> = TIncludeRef extends { multiple?: infer M }
|
|
169
|
+
? M extends true
|
|
170
|
+
? true
|
|
171
|
+
: false
|
|
172
|
+
: false
|
|
173
|
+
|
|
174
|
+
type IsIncludeRequired<TIncludeRef> = TIncludeRef extends { required?: infer R }
|
|
175
|
+
? R extends false
|
|
176
|
+
? false
|
|
177
|
+
: true
|
|
178
|
+
: true
|
|
179
|
+
|
|
180
|
+
type IsOptionalOutputInclude<TIncludeRef> = IsIncludeMultiple<TIncludeRef> extends true
|
|
181
|
+
? false
|
|
182
|
+
: IsIncludeRequired<TIncludeRef> extends false
|
|
183
|
+
? true
|
|
184
|
+
: false
|
|
185
|
+
|
|
186
|
+
type IsOptionalInputInclude<TIncludeRef> = IsIncludeRequired<TIncludeRef> extends false
|
|
187
|
+
? true
|
|
188
|
+
: false
|
|
189
|
+
|
|
190
|
+
type IncludeOutputValue<TIncludeRef> = IsIncludeMultiple<TIncludeRef> extends true
|
|
191
|
+
? EntityValue<ResolveIncludeEntity<TIncludeRef>>[]
|
|
192
|
+
: EntityValue<ResolveIncludeEntity<TIncludeRef>>
|
|
193
|
+
|
|
194
|
+
type IncludeInputValue<TIncludeRef> = IsIncludeMultiple<TIncludeRef> extends true
|
|
195
|
+
? EntityValueInput<ResolveIncludeEntity<TIncludeRef>>[]
|
|
196
|
+
: EntityValueInput<ResolveIncludeEntity<TIncludeRef>>
|
|
197
|
+
|
|
198
|
+
type InclusionValueShape<TIncludes extends Record<string, EntityIncludeRef>> =
|
|
199
|
+
TIncludes extends Record<string, never>
|
|
200
|
+
? Record<never, never>
|
|
201
|
+
: {
|
|
202
|
+
-readonly [K in keyof TIncludes as IsOptionalOutputInclude<TIncludes[K]> extends true
|
|
203
|
+
? never
|
|
204
|
+
: K]-?: IncludeOutputValue<TIncludes[K]>
|
|
205
|
+
} & {
|
|
206
|
+
-readonly [K in keyof TIncludes as IsOptionalOutputInclude<TIncludes[K]> extends true
|
|
207
|
+
? K
|
|
208
|
+
: never]?: IncludeOutputValue<TIncludes[K]>
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
type InclusionInputShape<TIncludes extends Record<string, EntityIncludeRef>> =
|
|
212
|
+
TIncludes extends Record<string, never>
|
|
213
|
+
? Record<never, never>
|
|
214
|
+
: {
|
|
215
|
+
-readonly [K in keyof TIncludes as IsOptionalInputInclude<TIncludes[K]> extends true
|
|
216
|
+
? never
|
|
217
|
+
: K]-?: IncludeInputValue<TIncludes[K]>
|
|
218
|
+
} & {
|
|
219
|
+
-readonly [K in keyof TIncludes as IsOptionalInputInclude<TIncludes[K]> extends true
|
|
220
|
+
? K
|
|
221
|
+
: never]?: IncludeInputValue<TIncludes[K]>
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
type DefineEntityValue<
|
|
225
|
+
TSchema extends z.ZodType,
|
|
226
|
+
TExtends extends Record<string, Entity>,
|
|
227
|
+
TIncludes extends Record<string, EntityIncludeRef>,
|
|
228
|
+
> = DistributiveSimplify<
|
|
229
|
+
z.output<MakeEntitySchema<AddSchemaExtensions<TSchema, TExtends>>> &
|
|
230
|
+
InclusionValueShape<TIncludes>
|
|
231
|
+
>
|
|
232
|
+
|
|
233
|
+
type DefineEntityInput<
|
|
234
|
+
TSchema extends z.ZodType,
|
|
235
|
+
TExtends extends Record<string, Entity>,
|
|
236
|
+
TIncludes extends Record<string, EntityIncludeRef>,
|
|
237
|
+
> = DistributiveSimplify<
|
|
238
|
+
z.input<MakeEntitySchema<AddSchemaExtensions<TSchema, TExtends>>> & InclusionInputShape<TIncludes>
|
|
239
|
+
>
|
|
240
|
+
|
|
241
|
+
type MakeEntitySchema<TSchema extends z.ZodTypeAny> = z.ZodIntersection<
|
|
242
|
+
TSchema,
|
|
243
|
+
typeof entityWithMetaSchema
|
|
244
|
+
>
|
|
245
|
+
|
|
122
246
|
type EntityOptions<
|
|
123
247
|
TType extends VersionedName,
|
|
124
248
|
TSchema extends z.ZodType,
|
|
@@ -160,32 +284,50 @@ type EntityOptions<
|
|
|
160
284
|
includes?: TIncludes
|
|
161
285
|
}
|
|
162
286
|
|
|
163
|
-
export type EntityIncludeRef =
|
|
287
|
+
export type EntityIncludeRef =
|
|
288
|
+
| Entity
|
|
289
|
+
| { entity: Entity; multiple?: boolean; required?: boolean }
|
|
290
|
+
| { entity: () => Entity; multiple?: boolean; required?: boolean }
|
|
291
|
+
|
|
292
|
+
type EntityIncludeObjectRef = Exclude<EntityIncludeRef, Entity>
|
|
293
|
+
|
|
294
|
+
function isEntityIncludeRef(value: EntityIncludeRef): value is EntityIncludeObjectRef {
|
|
295
|
+
if (typeof value !== "object" || value === null || !("entity" in value)) {
|
|
296
|
+
return false
|
|
297
|
+
}
|
|
164
298
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
): value is { entity: Entity; multiple?: boolean; required?: boolean } {
|
|
168
|
-
return typeof value === "object" && value !== null && "entity" in value
|
|
299
|
+
const entity = (value as { entity: unknown }).entity
|
|
300
|
+
return typeof entity === "function" || isEntity(entity)
|
|
169
301
|
}
|
|
170
302
|
|
|
171
303
|
type InclusionShape<TImplements extends Record<string, EntityIncludeRef>> = {
|
|
172
|
-
[K in keyof TImplements]: TImplements[K] extends {
|
|
173
|
-
entity: infer E
|
|
304
|
+
-readonly [K in keyof TImplements]: TImplements[K] extends {
|
|
305
|
+
entity: () => infer E
|
|
174
306
|
multiple?: infer M
|
|
175
307
|
required?: infer R
|
|
176
308
|
}
|
|
177
|
-
?
|
|
309
|
+
? M extends true
|
|
310
|
+
? R extends false
|
|
311
|
+
? z.ZodDefault<z.ZodArray<z.ZodType<EntityValue<E>>>>
|
|
312
|
+
: z.ZodArray<z.ZodType<EntityValue<E>>>
|
|
313
|
+
: R extends false
|
|
314
|
+
? z.ZodOptional<z.ZodType<EntityValue<E>>>
|
|
315
|
+
: z.ZodType<EntityValue<E>>
|
|
316
|
+
: TImplements[K] extends {
|
|
317
|
+
entity: infer E
|
|
318
|
+
multiple?: infer M
|
|
319
|
+
required?: infer R
|
|
320
|
+
}
|
|
178
321
|
? M extends true
|
|
179
322
|
? R extends false
|
|
180
|
-
? z.
|
|
181
|
-
: z.ZodArray<E
|
|
323
|
+
? z.ZodDefault<z.ZodArray<z.ZodType<EntityValue<E>>>>
|
|
324
|
+
: z.ZodArray<z.ZodType<EntityValue<E>>>
|
|
182
325
|
: R extends false
|
|
183
|
-
? z.ZodOptional<E
|
|
184
|
-
: E
|
|
185
|
-
:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
: never
|
|
326
|
+
? z.ZodOptional<z.ZodType<EntityValue<E>>>
|
|
327
|
+
: z.ZodType<EntityValue<E>>
|
|
328
|
+
: TImplements[K] extends Entity
|
|
329
|
+
? z.ZodType<EntityValue<TImplements[K]>>
|
|
330
|
+
: never
|
|
189
331
|
}
|
|
190
332
|
|
|
191
333
|
type AddSchemaExtensions<
|
|
@@ -216,6 +358,24 @@ type AddTypeExtensions<
|
|
|
216
358
|
>
|
|
217
359
|
}[keyof TExtends]
|
|
218
360
|
|
|
361
|
+
type AddIncludeExtensions<
|
|
362
|
+
TIncludes extends Record<string, EntityIncludeRef>,
|
|
363
|
+
TExtends extends Record<string, Entity>,
|
|
364
|
+
> = TExtends extends Record<string, never>
|
|
365
|
+
? TIncludes
|
|
366
|
+
: IsEmptyObject<TExtends> extends true
|
|
367
|
+
? TIncludes
|
|
368
|
+
: {
|
|
369
|
+
[K in keyof TExtends]: AddIncludeExtensions<
|
|
370
|
+
TIncludes & ExtractEntityIncludes<TExtends[K][typeof entityIncludes]>,
|
|
371
|
+
Omit<TExtends, K>
|
|
372
|
+
>
|
|
373
|
+
}[keyof TExtends]
|
|
374
|
+
|
|
375
|
+
type ExtractEntityIncludes<T> = T extends Record<string, EntityIncludeRef>
|
|
376
|
+
? T
|
|
377
|
+
: Record<string, never>
|
|
378
|
+
|
|
219
379
|
type AddSchemaInclusions<
|
|
220
380
|
TSchema extends z.ZodTypeAny,
|
|
221
381
|
TImplements extends Record<string, EntityIncludeRef>,
|
|
@@ -223,37 +383,20 @@ type AddSchemaInclusions<
|
|
|
223
383
|
? TSchema
|
|
224
384
|
: z.ZodIntersection<TSchema, z.ZodObject<InclusionShape<TImplements>>>
|
|
225
385
|
|
|
226
|
-
type AddTypeInclusions<
|
|
227
|
-
TImplementedTypes extends EntityTypes,
|
|
228
|
-
TImplements extends Record<string, EntityIncludeRef>,
|
|
229
|
-
> = TImplements extends Record<string, never>
|
|
230
|
-
? TImplementedTypes
|
|
231
|
-
: {
|
|
232
|
-
[K in keyof TImplements]: TImplements[K] extends {
|
|
233
|
-
entity: infer E
|
|
234
|
-
}
|
|
235
|
-
? E extends Entity
|
|
236
|
-
? AddTypeInclusions<TImplementedTypes & E[typeof implementedTypes], Omit<TImplements, K>>
|
|
237
|
-
: TImplementedTypes
|
|
238
|
-
: TImplements[K] extends Entity
|
|
239
|
-
? AddTypeInclusions<
|
|
240
|
-
TImplementedTypes & TImplements[K][typeof implementedTypes],
|
|
241
|
-
Omit<TImplements, K>
|
|
242
|
-
>
|
|
243
|
-
: TImplementedTypes
|
|
244
|
-
}[keyof TImplements]
|
|
245
|
-
|
|
246
386
|
export function defineEntity<
|
|
247
387
|
TType extends VersionedName,
|
|
248
388
|
TSchema extends z.ZodType,
|
|
249
|
-
TExtends extends Record<string, Entity> = Record<string, never>,
|
|
250
|
-
|
|
389
|
+
const TExtends extends Record<string, Entity> = Record<string, never>,
|
|
390
|
+
const TIncludes extends Record<string, EntityIncludeRef> = Record<never, never>,
|
|
251
391
|
>(
|
|
252
|
-
options: EntityOptions<TType, TSchema, TExtends,
|
|
392
|
+
options: EntityOptions<TType, TSchema, TExtends, TIncludes>,
|
|
253
393
|
): Entity<
|
|
254
394
|
TType,
|
|
255
|
-
Simplify<
|
|
256
|
-
AddSchemaExtensions<AddSchemaInclusions<TSchema,
|
|
395
|
+
Simplify<AddTypeExtensions<{ [K in TType]: true }, TExtends>>,
|
|
396
|
+
MakeEntitySchema<AddSchemaExtensions<AddSchemaInclusions<TSchema, TIncludes>, TExtends>>,
|
|
397
|
+
DefineEntityValue<TSchema, TExtends, AddIncludeExtensions<TIncludes, TExtends>>,
|
|
398
|
+
DefineEntityInput<TSchema, TExtends, AddIncludeExtensions<TIncludes, TExtends>>,
|
|
399
|
+
AddIncludeExtensions<TIncludes, TExtends>
|
|
257
400
|
> {
|
|
258
401
|
try {
|
|
259
402
|
entityModelSchema.shape.type.parse(options.type)
|
|
@@ -265,40 +408,48 @@ export function defineEntity<
|
|
|
265
408
|
throw new Error("Entity schema is required")
|
|
266
409
|
}
|
|
267
410
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
411
|
+
type IncludeRef = {
|
|
412
|
+
field: string
|
|
413
|
+
required: boolean
|
|
414
|
+
multiple: boolean
|
|
415
|
+
getEntity: () => Entity
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const includeRefs: IncludeRef[] = Object.entries(options.includes ?? {}).map(
|
|
419
|
+
([field, entityRef]) => {
|
|
420
|
+
if (isEntityIncludeRef(entityRef)) {
|
|
421
|
+
const required = entityRef.required ?? true
|
|
422
|
+
const multiple = entityRef.multiple ?? false
|
|
423
|
+
|
|
424
|
+
let getEntity: () => Entity
|
|
425
|
+
|
|
426
|
+
if (typeof entityRef.entity === "function") {
|
|
427
|
+
const entity = entityRef.entity
|
|
428
|
+
getEntity = entity
|
|
429
|
+
} else {
|
|
430
|
+
const entity = entityRef.entity
|
|
431
|
+
getEntity = () => entity
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return { field, required, multiple, getEntity }
|
|
278
435
|
}
|
|
279
|
-
}
|
|
280
436
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const inclusionShape = includedEntities.reduce(
|
|
293
|
-
(shape, { entity, inclusion }) => {
|
|
294
|
-
if (inclusion.multiple) {
|
|
295
|
-
shape[inclusion.field] = inclusion.required
|
|
296
|
-
? entity.schema.array()
|
|
297
|
-
: entity.schema.array().optional()
|
|
437
|
+
const getEntity = () => entityRef
|
|
438
|
+
return { field, required: true, multiple: false, getEntity }
|
|
439
|
+
},
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
const inclusionShape = includeRefs.reduce(
|
|
443
|
+
(shape, includeRef) => {
|
|
444
|
+
let schema: z.ZodTypeAny = z.lazy(() => includeRef.getEntity().schema)
|
|
445
|
+
|
|
446
|
+
if (includeRef.multiple) {
|
|
447
|
+
schema = includeRef.required ? schema.array().min(1) : schema.array().default([])
|
|
298
448
|
} else {
|
|
299
|
-
|
|
449
|
+
schema = includeRef.required ? schema : schema.optional()
|
|
300
450
|
}
|
|
301
451
|
|
|
452
|
+
shape[includeRef.field] = schema
|
|
302
453
|
return shape
|
|
303
454
|
},
|
|
304
455
|
{} as Record<string, z.ZodTypeAny>,
|
|
@@ -309,23 +460,32 @@ export function defineEntity<
|
|
|
309
460
|
options.schema as z.ZodType,
|
|
310
461
|
)
|
|
311
462
|
|
|
312
|
-
if (
|
|
463
|
+
if (includeRefs.length > 0) {
|
|
313
464
|
finalSchema = z.intersection(finalSchema, z.object(inclusionShape))
|
|
314
465
|
}
|
|
315
466
|
|
|
316
|
-
|
|
467
|
+
finalSchema = z.intersection(finalSchema, entityWithMetaSchema)
|
|
468
|
+
|
|
469
|
+
const directInclusions = () =>
|
|
470
|
+
includeRefs.map(includeRef => ({
|
|
471
|
+
type: includeRef.getEntity().type,
|
|
472
|
+
required: includeRef.required,
|
|
473
|
+
multiple: includeRef.multiple,
|
|
474
|
+
field: includeRef.field,
|
|
475
|
+
}))
|
|
317
476
|
const directExtensions = Object.values(options.extends ?? {}).map(entity => entity.type)
|
|
318
477
|
|
|
319
|
-
const
|
|
320
|
-
|
|
478
|
+
const getInclusions = () => {
|
|
479
|
+
const incs = [...directInclusions()]
|
|
480
|
+
|
|
481
|
+
for (const entity of Object.values(options.extends ?? {})) {
|
|
321
482
|
if (entity.model.inclusions) {
|
|
322
483
|
incs.push(...entity.model.inclusions)
|
|
323
484
|
}
|
|
485
|
+
}
|
|
324
486
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
[...directInclusions],
|
|
328
|
-
)
|
|
487
|
+
return incs
|
|
488
|
+
}
|
|
329
489
|
|
|
330
490
|
const extensions = Object.values(options.extends ?? {}).reduce((exts, entity) => {
|
|
331
491
|
exts.push(...(entity.model.extensions ?? []), entity.type)
|
|
@@ -343,12 +503,23 @@ export function defineEntity<
|
|
|
343
503
|
type: options.type,
|
|
344
504
|
extensions: extensions.length > 0 ? extensions : undefined,
|
|
345
505
|
directExtensions: directExtensions.length > 0 ? directExtensions : undefined,
|
|
346
|
-
|
|
347
|
-
|
|
506
|
+
get inclusions() {
|
|
507
|
+
const incs = getInclusions()
|
|
508
|
+
return incs.length > 0 ? incs : undefined
|
|
509
|
+
},
|
|
510
|
+
get directInclusions() {
|
|
511
|
+
const incs = directInclusions()
|
|
512
|
+
return incs.length > 0 ? incs : undefined
|
|
513
|
+
},
|
|
348
514
|
get schema() {
|
|
349
515
|
if (!_schema) {
|
|
350
516
|
// TODO: forbid unrepresentable types (and find way to filter out "undefined" literals)
|
|
351
|
-
|
|
517
|
+
const rawSchema = z.toJSONSchema(finalSchema, {
|
|
518
|
+
target: "draft-7",
|
|
519
|
+
unrepresentable: "any",
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
_schema = fixJsonSchema(rawSchema) as z.core.JSONSchema.BaseSchema
|
|
352
523
|
}
|
|
353
524
|
|
|
354
525
|
return _schema
|
|
@@ -395,3 +566,72 @@ export function isAssignableTo(entity: EntityModel, target: VersionedName): bool
|
|
|
395
566
|
|
|
396
567
|
return entity.inclusions?.some(implementation => implementation.type === target) ?? false
|
|
397
568
|
}
|
|
569
|
+
|
|
570
|
+
export const entityMetaSchema = z.object({
|
|
571
|
+
/**
|
|
572
|
+
* The type of the entity.
|
|
573
|
+
*/
|
|
574
|
+
type: versionedNameSchema,
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* The globally unique identity of the entity.
|
|
578
|
+
*
|
|
579
|
+
* Anonymous entities are forbidden.
|
|
580
|
+
*/
|
|
581
|
+
identity: z.string(),
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* The IDs of the entities to reference by this entity.
|
|
585
|
+
* Must already exist in the system (or be at least defined by this unit).
|
|
586
|
+
*/
|
|
587
|
+
references: z.record(z.string(), z.string().array()).optional(),
|
|
588
|
+
|
|
589
|
+
...objectMetaSchema.pick({
|
|
590
|
+
title: true,
|
|
591
|
+
description: true,
|
|
592
|
+
icon: true,
|
|
593
|
+
iconColor: true,
|
|
594
|
+
}).shape,
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
export type EntityMeta = z.infer<typeof entityMetaSchema>
|
|
598
|
+
|
|
599
|
+
export const entityWithMetaSchema = z.object({
|
|
600
|
+
$meta: entityMetaSchema,
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
export type EntityWithMeta = z.infer<typeof entityWithMetaSchema>
|
|
604
|
+
|
|
605
|
+
export type EntityWithRequiredMeta = EntityWithMeta & {
|
|
606
|
+
$meta: EntityMeta
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const entityIdCache = new WeakMap<EntityWithMeta, string>()
|
|
610
|
+
const entityIdNamespace = "3cd37048-7c50-43a9-a2b9-ff7ff2b5ee79"
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Gets the unique ID of an entity based on its type and identity.
|
|
614
|
+
*
|
|
615
|
+
* The ID is generated using a hash of the entity's type and identity, and is cached for future retrievals.
|
|
616
|
+
*
|
|
617
|
+
* @param entity The entity to get the ID for.
|
|
618
|
+
* @returns The unique ID of the entity.
|
|
619
|
+
*/
|
|
620
|
+
export function getEntityId(entity: EntityWithMeta): string {
|
|
621
|
+
if (!entity.$meta) {
|
|
622
|
+
throw new Error("Entity $meta is required to generate an entity id")
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const existingId = entityIdCache.get(entity)
|
|
626
|
+
|
|
627
|
+
if (existingId) {
|
|
628
|
+
return existingId
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const { type, identity } = entity.$meta
|
|
632
|
+
|
|
633
|
+
const id = cuidv2d(entityIdNamespace, `${type}:${identity}`)
|
|
634
|
+
entityIdCache.set(entity, id)
|
|
635
|
+
|
|
636
|
+
return id
|
|
637
|
+
}
|