@echovisionlab/geul-common 0.1.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.
Files changed (47) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +37 -0
  3. package/package.json +89 -0
  4. package/src/collaboration/artist.ts +63 -0
  5. package/src/collaboration/block-room-codec/ai-document-applicator.ts +319 -0
  6. package/src/collaboration/block-room-codec/ai-document-field-mutations.ts +557 -0
  7. package/src/collaboration/block-room-codec/ai-document-page-structure-mutations.ts +449 -0
  8. package/src/collaboration/block-room-codec/ai-document-values.ts +368 -0
  9. package/src/collaboration/block-room-codec/hydration.ts +257 -0
  10. package/src/collaboration/block-room-codec/internal.ts +432 -0
  11. package/src/collaboration/block-room-codec/locale-change-validation.ts +237 -0
  12. package/src/collaboration/block-room-codec/locale-presence.ts +827 -0
  13. package/src/collaboration/block-room-codec/materialization.ts +450 -0
  14. package/src/collaboration/block-room-codec/observation.ts +518 -0
  15. package/src/collaboration/block-room-codec/payload-mutations.ts +347 -0
  16. package/src/collaboration/block-room-codec/room-access.ts +154 -0
  17. package/src/collaboration/block-room-codec/structure-mutations.ts +456 -0
  18. package/src/collaboration/block-room-codec.ts +86 -0
  19. package/src/collaboration/campaign.ts +37 -0
  20. package/src/collaboration/document-layout.ts +75 -0
  21. package/src/collaboration/document.ts +185 -0
  22. package/src/collaboration/email-layout.ts +150 -0
  23. package/src/collaboration/form.ts +513 -0
  24. package/src/collaboration/label.ts +49 -0
  25. package/src/collaboration/map-theme.ts +157 -0
  26. package/src/collaboration/member-id.ts +9 -0
  27. package/src/collaboration/menu.ts +258 -0
  28. package/src/collaboration/metadata-ai.ts +99 -0
  29. package/src/collaboration/page.ts +483 -0
  30. package/src/collaboration/post-series.ts +96 -0
  31. package/src/collaboration/post.ts +59 -0
  32. package/src/collaboration/release.ts +221 -0
  33. package/src/collaboration/runtime-events.ts +547 -0
  34. package/src/collaboration/work.ts +107 -0
  35. package/src/editor/link-normalization.ts +212 -0
  36. package/src/editor/materialized-blocks.ts +58 -0
  37. package/src/index.ts +22 -0
  38. package/src/media/block-schemas.ts +78 -0
  39. package/src/media/hydration.ts +226 -0
  40. package/src/page/block-fixtures.ts +586 -0
  41. package/src/page/index.ts +3 -0
  42. package/src/page/types.ts +57 -0
  43. package/src/post/index.ts +1 -0
  44. package/src/post/types.ts +13 -0
  45. package/src/test/random-id.ts +9 -0
  46. package/src/translation/release.ts +88 -0
  47. package/src/types.ts +14 -0
@@ -0,0 +1,547 @@
1
+ import { z } from "zod";
2
+
3
+ export type RuntimeEntityType =
4
+ | "post"
5
+ | "page"
6
+ | "work"
7
+ | "artist"
8
+ | "label"
9
+ | "release"
10
+ | "series"
11
+ | "form"
12
+ | "program_event"
13
+ | "menu"
14
+ | "site"
15
+ | "site_setting"
16
+ | "email_template"
17
+ | "email_layout"
18
+ | "campaign"
19
+ | "privacy"
20
+ | "terms";
21
+
22
+ export type TranslationLifecycleRuntimeStatus = "failed";
23
+
24
+ export type MediaProcessingRuntimeStatus = "processing" | "ready" | "failed";
25
+
26
+ export type OgGenerationRuntimeEntityType =
27
+ | "post"
28
+ | "page"
29
+ | "work"
30
+ | "artist"
31
+ | "label"
32
+ | "release"
33
+ | "series"
34
+ | "form"
35
+ | "site"
36
+ | "privacy"
37
+ | "terms";
38
+
39
+ export type OgGenerationRuntimeStatus =
40
+ "queued" | "processing" | "ready" | "failed" | "superseded" | "cancelled";
41
+
42
+ export function isOgGenerationRuntimeTerminal(
43
+ status: OgGenerationRuntimeStatus,
44
+ ): boolean {
45
+ return (
46
+ status === "ready" ||
47
+ status === "failed" ||
48
+ status === "superseded" ||
49
+ status === "cancelled"
50
+ );
51
+ }
52
+
53
+ export type FileIngestRuntimeSource = "upload" | "embed";
54
+
55
+ export type FileIngestRuntimeStage =
56
+ | "uploading"
57
+ | "downloading"
58
+ | "finalized"
59
+ | "attached"
60
+ | "completed"
61
+ | "expired"
62
+ | "failed"
63
+ | "unknown";
64
+
65
+ export interface RuntimeEventEnvelope<K extends string, P> {
66
+ version: 1;
67
+ kind: K;
68
+ entityType: RuntimeEntityType;
69
+ entityId: string;
70
+ locale?: string;
71
+ correlationId?: string;
72
+ sequence?: number;
73
+ timestampMs: number;
74
+ payload: P;
75
+ }
76
+
77
+ export interface OgGenerationLifecycleRuntimePayload {
78
+ generationId: string;
79
+ runId: string;
80
+ status: OgGenerationRuntimeStatus;
81
+ assetId?: string;
82
+ assetUrl?: string;
83
+ errorCode?: string;
84
+ error?: string;
85
+ replacementGenerationId?: string;
86
+ }
87
+
88
+ export interface RenderCompleteRuntimePayload {
89
+ contentHash: string;
90
+ success: boolean;
91
+ error?: string;
92
+ }
93
+
94
+ export interface MediaProcessingRuntimeOutputs {
95
+ spectrogramAssetId?: string;
96
+ thumbnailAssetId?: string;
97
+ hlsGenerationId?: string;
98
+ durationSeconds?: number;
99
+ waveformAssetId?: string;
100
+ }
101
+
102
+ export interface MediaProcessingLifecycleRuntimePayload {
103
+ fileId: string;
104
+ slotId?: string;
105
+ attemptId?: string;
106
+ status: MediaProcessingRuntimeStatus;
107
+ percentage?: number;
108
+ outputs?: MediaProcessingRuntimeOutputs;
109
+ error?: string;
110
+ trackId?: string;
111
+ releaseId?: string;
112
+ }
113
+
114
+ export function isMediaProcessingRuntimeReady(payload: {
115
+ status?: MediaProcessingRuntimeStatus | null;
116
+ }): boolean {
117
+ return payload.status === "ready";
118
+ }
119
+
120
+ export interface FileIngestLifecycleRuntimePayload {
121
+ fileId: string;
122
+ uploadId?: string;
123
+ mediaKind?: string;
124
+ slotId?: string;
125
+ attemptId?: string;
126
+ source: FileIngestRuntimeSource;
127
+ stage: FileIngestRuntimeStage;
128
+ progress: number;
129
+ bytesCompleted?: number;
130
+ bytesTotal?: number;
131
+ fileName?: string;
132
+ mimeType?: string;
133
+ fileSize?: number;
134
+ error?: string;
135
+ trackId?: string;
136
+ releaseId?: string;
137
+ }
138
+
139
+ export interface TranslationLifecycleRuntimePayload {
140
+ jobId: string;
141
+ targetLocale: string;
142
+ status: TranslationLifecycleRuntimeStatus;
143
+ error?: string;
144
+ }
145
+
146
+ export interface RuntimeErrorPayload {
147
+ code: string;
148
+ message: string;
149
+ retryable?: boolean;
150
+ source?: "backend" | "collab" | "worker";
151
+ details?: Record<string, string | number | boolean | null>;
152
+ }
153
+
154
+ export interface OgGenerationLifecycleRuntimeEvent extends RuntimeEventEnvelope<
155
+ "og.lifecycle",
156
+ OgGenerationLifecycleRuntimePayload
157
+ > {
158
+ entityType: OgGenerationRuntimeEntityType;
159
+ correlationId: string;
160
+ }
161
+ export type RenderCompleteRuntimeEvent = RuntimeEventEnvelope<
162
+ "render.complete",
163
+ RenderCompleteRuntimePayload
164
+ >;
165
+ export type MediaProcessingLifecycleRuntimeEvent = RuntimeEventEnvelope<
166
+ "media.processing.lifecycle",
167
+ MediaProcessingLifecycleRuntimePayload
168
+ >;
169
+ export type FileIngestLifecycleRuntimeEvent = RuntimeEventEnvelope<
170
+ "file.ingest.lifecycle",
171
+ FileIngestLifecycleRuntimePayload
172
+ >;
173
+ export type TranslationLifecycleRuntimeEvent = RuntimeEventEnvelope<
174
+ "translation.lifecycle",
175
+ TranslationLifecycleRuntimePayload
176
+ >;
177
+ export type RuntimeErrorEvent = RuntimeEventEnvelope<
178
+ "runtime.error",
179
+ RuntimeErrorPayload
180
+ >;
181
+
182
+ export type EditorRuntimeEvent =
183
+ | OgGenerationLifecycleRuntimeEvent
184
+ | RenderCompleteRuntimeEvent
185
+ | MediaProcessingLifecycleRuntimeEvent
186
+ | FileIngestLifecycleRuntimeEvent
187
+ | TranslationLifecycleRuntimeEvent
188
+ | RuntimeErrorEvent;
189
+
190
+ export const runtimeEntityTypeSchema = z.enum([
191
+ "post",
192
+ "page",
193
+ "work",
194
+ "artist",
195
+ "label",
196
+ "release",
197
+ "series",
198
+ "form",
199
+ "program_event",
200
+ "menu",
201
+ "site",
202
+ "site_setting",
203
+ "email_template",
204
+ "email_layout",
205
+ "campaign",
206
+ "privacy",
207
+ "terms",
208
+ ]);
209
+
210
+ export const mediaProcessingRuntimeStatusSchema = z.enum([
211
+ "processing",
212
+ "ready",
213
+ "failed",
214
+ ]);
215
+
216
+ export const ogGenerationRuntimeEntityTypeSchema = z.enum([
217
+ "post",
218
+ "page",
219
+ "work",
220
+ "artist",
221
+ "label",
222
+ "release",
223
+ "series",
224
+ "form",
225
+ "site",
226
+ "privacy",
227
+ "terms",
228
+ ]);
229
+
230
+ export const ogGenerationRuntimeStatusSchema = z.enum([
231
+ "queued",
232
+ "processing",
233
+ "ready",
234
+ "failed",
235
+ "superseded",
236
+ "cancelled",
237
+ ]);
238
+
239
+ export const ogGenerationLifecycleRuntimePayloadSchema = z
240
+ .object({
241
+ generationId: z.string().min(1),
242
+ runId: z.string().min(1),
243
+ status: ogGenerationRuntimeStatusSchema,
244
+ assetId: z.string().min(1).optional(),
245
+ assetUrl: z.string().min(1).optional(),
246
+ errorCode: z.string().min(1).optional(),
247
+ error: z.string().min(1).optional(),
248
+ replacementGenerationId: z.string().min(1).optional(),
249
+ })
250
+ .strict()
251
+ .superRefine((payload, ctx) => {
252
+ if (payload.status === "ready" && !payload.assetId) {
253
+ ctx.addIssue({
254
+ code: z.ZodIssueCode.custom,
255
+ path: ["assetId"],
256
+ message: "ready OG lifecycle requires assetId",
257
+ });
258
+ }
259
+ if (payload.status === "failed" && !payload.error) {
260
+ ctx.addIssue({
261
+ code: z.ZodIssueCode.custom,
262
+ path: ["error"],
263
+ message: "failed OG lifecycle requires error",
264
+ });
265
+ }
266
+ if (payload.status === "superseded" && !payload.replacementGenerationId) {
267
+ ctx.addIssue({
268
+ code: z.ZodIssueCode.custom,
269
+ path: ["replacementGenerationId"],
270
+ message: "superseded OG lifecycle requires replacementGenerationId",
271
+ });
272
+ }
273
+ if (payload.status !== "ready" && (payload.assetId || payload.assetUrl)) {
274
+ ctx.addIssue({
275
+ code: z.ZodIssueCode.custom,
276
+ path: ["assetId"],
277
+ message: "only ready OG lifecycle may include an asset",
278
+ });
279
+ }
280
+ if (payload.status !== "superseded" && payload.replacementGenerationId) {
281
+ ctx.addIssue({
282
+ code: z.ZodIssueCode.custom,
283
+ path: ["replacementGenerationId"],
284
+ message: "only superseded OG lifecycle may include a replacement",
285
+ });
286
+ }
287
+ });
288
+
289
+ export const mediaProcessingRuntimeOutputsSchema = z
290
+ .object({
291
+ spectrogramAssetId: z.string().optional(),
292
+ thumbnailAssetId: z.string().optional(),
293
+ hlsGenerationId: z.string().optional(),
294
+ durationSeconds: z.number().int().nonnegative().optional(),
295
+ waveformAssetId: z.string().optional(),
296
+ })
297
+ .strict();
298
+
299
+ export const mediaProcessingLifecycleRuntimePayloadSchema = z
300
+ .object({
301
+ fileId: z.string().min(1),
302
+ slotId: z.string().optional(),
303
+ attemptId: z.string().optional(),
304
+ status: mediaProcessingRuntimeStatusSchema,
305
+ percentage: z.number().int().min(0).max(100).optional(),
306
+ outputs: mediaProcessingRuntimeOutputsSchema.optional(),
307
+ error: z.string().optional(),
308
+ trackId: z.string().optional(),
309
+ releaseId: z.string().optional(),
310
+ })
311
+ .strict()
312
+ .superRefine((payload, ctx) => {
313
+ if (
314
+ payload.status === "processing" &&
315
+ typeof payload.percentage !== "number"
316
+ ) {
317
+ ctx.addIssue({
318
+ code: z.ZodIssueCode.custom,
319
+ path: ["percentage"],
320
+ message: "processing media lifecycle requires percentage",
321
+ });
322
+ }
323
+ if (payload.status === "processing" && payload.outputs) {
324
+ ctx.addIssue({
325
+ code: z.ZodIssueCode.custom,
326
+ path: ["outputs"],
327
+ message: "processing media lifecycle must not include ready outputs",
328
+ });
329
+ }
330
+ if (payload.status === "ready" && !payload.outputs) {
331
+ ctx.addIssue({
332
+ code: z.ZodIssueCode.custom,
333
+ path: ["outputs"],
334
+ message: "ready media lifecycle requires outputs",
335
+ });
336
+ }
337
+ if (payload.status === "ready" && typeof payload.percentage === "number") {
338
+ ctx.addIssue({
339
+ code: z.ZodIssueCode.custom,
340
+ path: ["percentage"],
341
+ message: "ready media lifecycle must not include processing percentage",
342
+ });
343
+ }
344
+ if (payload.status === "failed" && payload.outputs) {
345
+ ctx.addIssue({
346
+ code: z.ZodIssueCode.custom,
347
+ path: ["outputs"],
348
+ message: "failed media lifecycle must not include ready outputs",
349
+ });
350
+ }
351
+ if (payload.status === "failed" && typeof payload.percentage === "number") {
352
+ ctx.addIssue({
353
+ code: z.ZodIssueCode.custom,
354
+ path: ["percentage"],
355
+ message:
356
+ "failed media lifecycle must not include processing percentage",
357
+ });
358
+ }
359
+ });
360
+
361
+ export const fileIngestLifecycleRuntimePayloadSchema = z
362
+ .object({
363
+ fileId: z.string().min(1),
364
+ uploadId: z.string().optional(),
365
+ mediaKind: z.string().optional(),
366
+ slotId: z.string().optional(),
367
+ attemptId: z.string().optional(),
368
+ source: z.enum(["upload", "embed"]),
369
+ stage: z.enum([
370
+ "uploading",
371
+ "downloading",
372
+ "finalized",
373
+ "attached",
374
+ "completed",
375
+ "expired",
376
+ "failed",
377
+ "unknown",
378
+ ]),
379
+ progress: z.number().int().min(0).max(100),
380
+ bytesCompleted: z.number().int().nonnegative().optional(),
381
+ bytesTotal: z.number().int().nonnegative().optional(),
382
+ fileName: z.string().optional(),
383
+ mimeType: z.string().optional(),
384
+ fileSize: z.number().int().nonnegative().optional(),
385
+ error: z.string().optional(),
386
+ trackId: z.string().optional(),
387
+ releaseId: z.string().optional(),
388
+ })
389
+ .strict();
390
+
391
+ const localeScopedOgEntityTypes = new Set<OgGenerationRuntimeEntityType>([
392
+ "post",
393
+ "page",
394
+ "form",
395
+ "privacy",
396
+ "terms",
397
+ ]);
398
+
399
+ const ogGenerationLifecycleRuntimeEventSchema = z
400
+ .object({
401
+ version: z.literal(1),
402
+ kind: z.literal("og.lifecycle"),
403
+ entityType: ogGenerationRuntimeEntityTypeSchema,
404
+ entityId: z.string().min(1),
405
+ locale: z.string().min(1).optional(),
406
+ correlationId: z.string().min(1),
407
+ sequence: z.number().int().optional(),
408
+ timestampMs: z.number().int(),
409
+ payload: ogGenerationLifecycleRuntimePayloadSchema,
410
+ })
411
+ .strict()
412
+ .superRefine((event, ctx) => {
413
+ const requiresLocale = localeScopedOgEntityTypes.has(event.entityType);
414
+ if (requiresLocale && !event.locale) {
415
+ ctx.addIssue({
416
+ code: z.ZodIssueCode.custom,
417
+ path: ["locale"],
418
+ message: `${event.entityType} OG lifecycle requires a locale target`,
419
+ });
420
+ }
421
+ if (!requiresLocale && event.locale) {
422
+ ctx.addIssue({
423
+ code: z.ZodIssueCode.custom,
424
+ path: ["locale"],
425
+ message: `${event.entityType} OG lifecycle requires an entity target`,
426
+ });
427
+ }
428
+ if (event.correlationId !== event.payload.generationId) {
429
+ ctx.addIssue({
430
+ code: z.ZodIssueCode.custom,
431
+ path: ["correlationId"],
432
+ message: "OG lifecycle correlationId must equal payload.generationId",
433
+ });
434
+ }
435
+ });
436
+
437
+ export const translationLifecycleRuntimePayloadSchema = z
438
+ .object({
439
+ jobId: z.string().min(1),
440
+ targetLocale: z.string().min(1),
441
+ status: z.literal("failed"),
442
+ error: z.string().optional(),
443
+ })
444
+ .strict();
445
+
446
+ export const editorRuntimeEventSchema = z.discriminatedUnion("kind", [
447
+ ogGenerationLifecycleRuntimeEventSchema,
448
+ z
449
+ .object({
450
+ version: z.literal(1),
451
+ kind: z.literal("render.complete"),
452
+ entityType: runtimeEntityTypeSchema,
453
+ entityId: z.string().min(1),
454
+ locale: z.string().optional(),
455
+ correlationId: z.string().optional(),
456
+ sequence: z.number().int().optional(),
457
+ timestampMs: z.number().int(),
458
+ payload: z
459
+ .object({
460
+ contentHash: z.string(),
461
+ success: z.boolean(),
462
+ error: z.string().optional(),
463
+ })
464
+ .strict(),
465
+ })
466
+ .strict(),
467
+ z
468
+ .object({
469
+ version: z.literal(1),
470
+ kind: z.literal("media.processing.lifecycle"),
471
+ entityType: runtimeEntityTypeSchema,
472
+ entityId: z.string().min(1),
473
+ locale: z.string().optional(),
474
+ correlationId: z.string().optional(),
475
+ sequence: z.number().int().optional(),
476
+ timestampMs: z.number().int(),
477
+ payload: mediaProcessingLifecycleRuntimePayloadSchema,
478
+ })
479
+ .strict(),
480
+ z
481
+ .object({
482
+ version: z.literal(1),
483
+ kind: z.literal("file.ingest.lifecycle"),
484
+ entityType: runtimeEntityTypeSchema,
485
+ entityId: z.string().min(1),
486
+ locale: z.string().optional(),
487
+ correlationId: z.string().optional(),
488
+ sequence: z.number().int().optional(),
489
+ timestampMs: z.number().int(),
490
+ payload: fileIngestLifecycleRuntimePayloadSchema,
491
+ })
492
+ .strict(),
493
+ z
494
+ .object({
495
+ version: z.literal(1),
496
+ kind: z.literal("translation.lifecycle"),
497
+ entityType: runtimeEntityTypeSchema,
498
+ entityId: z.string().min(1),
499
+ locale: z.string().optional(),
500
+ correlationId: z.string().optional(),
501
+ sequence: z.number().int().optional(),
502
+ timestampMs: z.number().int(),
503
+ payload: translationLifecycleRuntimePayloadSchema,
504
+ })
505
+ .strict(),
506
+ z
507
+ .object({
508
+ version: z.literal(1),
509
+ kind: z.literal("runtime.error"),
510
+ entityType: runtimeEntityTypeSchema,
511
+ entityId: z.string().min(1),
512
+ locale: z.string().optional(),
513
+ correlationId: z.string().optional(),
514
+ sequence: z.number().int().optional(),
515
+ timestampMs: z.number().int(),
516
+ payload: z
517
+ .object({
518
+ code: z.string().min(1),
519
+ message: z.string(),
520
+ retryable: z.boolean().optional(),
521
+ source: z.enum(["backend", "collab", "worker"]).optional(),
522
+ details: z
523
+ .record(
524
+ z.string(),
525
+ z.union([z.string(), z.number(), z.boolean(), z.null()]),
526
+ )
527
+ .optional(),
528
+ })
529
+ .strict(),
530
+ })
531
+ .strict(),
532
+ ]);
533
+
534
+ export function parseEditorRuntimeEventMessage(
535
+ payload: string,
536
+ ): EditorRuntimeEvent | null {
537
+ try {
538
+ const data = JSON.parse(payload) as unknown;
539
+ const result = editorRuntimeEventSchema.safeParse(data);
540
+ if (!result.success) {
541
+ return null;
542
+ }
543
+ return result.data as EditorRuntimeEvent;
544
+ } catch {
545
+ return null;
546
+ }
547
+ }
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+
3
+ export const workCollabFieldsSchema = z
4
+ .object({
5
+ title: z.string().optional(),
6
+ slug: z.string().nullable().optional(),
7
+ type: z.string().optional(),
8
+ summary: z.string().optional(),
9
+ featured: z.boolean().optional(),
10
+ year: z.number().int().min(1).max(9999).optional(),
11
+ month: z.number().int().min(1).max(12).optional(),
12
+ untilYear: z.number().int().min(1).max(9999).nullable().optional(),
13
+ untilMonth: z.number().int().min(1).max(12).nullable().optional(),
14
+ isPresent: z.boolean().optional(),
15
+ metadata: z.record(z.string(), z.unknown()).optional(),
16
+ featuredImageFileId: z.string().nullable().optional(),
17
+ creditsVersion: z.number().optional(),
18
+ creditOrder: z
19
+ .array(
20
+ z.discriminatedUnion("type", [
21
+ z.object({ type: z.literal("group"), id: z.string() }),
22
+ z.object({
23
+ type: z.literal("credit"),
24
+ id: z.string(),
25
+ creditType: z.enum(["artist", "member", "name"]),
26
+ }),
27
+ ]),
28
+ )
29
+ .optional(),
30
+ clients: z.array(z.string()).optional(), // Ordered list of client IDs
31
+ })
32
+ .strict();
33
+
34
+ export const WORK_JSON_KEYS: ReadonlySet<
35
+ keyof z.infer<typeof workCollabFieldsSchema>
36
+ > = new Set(["metadata", "creditOrder", "clients"]);
37
+
38
+ export const WORK_SOURCE_OWNED_FIELD_KEYS = ["title", "summary"] as const;
39
+ export const WORK_SHARED_FIELD_KEYS = [
40
+ "slug",
41
+ "type",
42
+ "featured",
43
+ "year",
44
+ "month",
45
+ "untilYear",
46
+ "untilMonth",
47
+ "isPresent",
48
+ "metadata",
49
+ "featuredImageFileId",
50
+ "creditsVersion",
51
+ "creditOrder",
52
+ "clients",
53
+ ] as const;
54
+
55
+ export type WorkCollabFields = z.infer<typeof workCollabFieldsSchema>;
56
+ export type WorkFieldValue =
57
+ string | boolean | number | unknown[] | Record<string, unknown> | null;
58
+ export type WorkSourceOwnedFieldKey =
59
+ (typeof WORK_SOURCE_OWNED_FIELD_KEYS)[number];
60
+ export type WorkSharedFieldKey = (typeof WORK_SHARED_FIELD_KEYS)[number];
61
+
62
+ export function extractWorkFields(metaMap: {
63
+ get(key: string): WorkFieldValue | undefined;
64
+ }): WorkCollabFields {
65
+ const raw: Record<string, unknown> = {};
66
+
67
+ for (const key of Object.keys(workCollabFieldsSchema.shape)) {
68
+ let value = metaMap.get(key);
69
+
70
+ if (
71
+ WORK_JSON_KEYS.has(key as keyof WorkCollabFields) &&
72
+ typeof value === "string"
73
+ ) {
74
+ try {
75
+ value = JSON.parse(value) as WorkFieldValue;
76
+ } catch {
77
+ throw new Error(`Failed to parse JSON for work field "${key}"`);
78
+ }
79
+ }
80
+
81
+ if (value !== undefined) {
82
+ raw[key] = value;
83
+ }
84
+ }
85
+
86
+ return workCollabFieldsSchema.parse(raw);
87
+ }
88
+
89
+ export function pickWorkSharedFields(
90
+ fields: WorkCollabFields,
91
+ ): Pick<WorkCollabFields, WorkSharedFieldKey> {
92
+ return {
93
+ slug: fields.slug,
94
+ type: fields.type,
95
+ featured: fields.featured,
96
+ year: fields.year,
97
+ month: fields.month,
98
+ untilYear: fields.untilYear,
99
+ untilMonth: fields.untilMonth,
100
+ isPresent: fields.isPresent,
101
+ metadata: fields.metadata,
102
+ featuredImageFileId: fields.featuredImageFileId,
103
+ creditsVersion: fields.creditsVersion,
104
+ creditOrder: fields.creditOrder,
105
+ clients: fields.clients,
106
+ };
107
+ }