@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.
@@ -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 = Entity | { entity: Entity; multiple?: boolean; required?: boolean }
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
- function isEntityIncludeRef(
166
- value: EntityIncludeRef,
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
- ? E extends Entity
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.ZodOptional<z.ZodArray<E["schema"]>>
181
- : z.ZodArray<E["schema"]>
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["schema"]>
184
- : E["schema"]
185
- : never
186
- : TImplements[K] extends Entity
187
- ? TImplements[K]["schema"]
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
- TImplements extends Record<string, EntityIncludeRef> = Record<string, never>,
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, TImplements>,
392
+ options: EntityOptions<TType, TSchema, TExtends, TIncludes>,
253
393
  ): Entity<
254
394
  TType,
255
- Simplify<AddTypeInclusions<AddTypeExtensions<{ [K in TType]: true }, TExtends>, TImplements>>,
256
- AddSchemaExtensions<AddSchemaInclusions<TSchema, TImplements>, TExtends>
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
- const includedEntities = Object.entries(options.includes ?? {}).map(([field, entityRef]) => {
269
- if (isEntityIncludeRef(entityRef)) {
270
- return {
271
- entity: entityRef.entity,
272
- inclusion: {
273
- type: entityRef.entity.type,
274
- required: entityRef.required ?? true,
275
- multiple: entityRef.multiple ?? false,
276
- field,
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
- return {
282
- entity: entityRef,
283
- inclusion: {
284
- type: entityRef.type,
285
- required: true,
286
- multiple: false,
287
- field,
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
- shape[inclusion.field] = inclusion.required ? entity.schema : entity.schema.optional()
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 (includedEntities.length > 0) {
463
+ if (includeRefs.length > 0) {
313
464
  finalSchema = z.intersection(finalSchema, z.object(inclusionShape))
314
465
  }
315
466
 
316
- const directInclusions = includedEntities.map(({ inclusion }) => inclusion)
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 inclusions = Object.values(options.extends ?? {}).reduce(
320
- (incs, entity) => {
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
- return incs
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
- inclusions: inclusions.length > 0 ? inclusions : undefined,
347
- directInclusions: directInclusions.length > 0 ? directInclusions : undefined,
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
- _schema = z.toJSONSchema(finalSchema, { target: "draft-7", unrepresentable: "any" })
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
+ }