|
1 | | -import { GoogleDocsReviewFixture } from './types'; |
| 1 | +import { |
| 2 | + type ReviewAsset, |
| 3 | + type ReviewEntryBlockGraph, |
| 4 | + type ReviewNormalizedDocument, |
| 5 | + type GoogleDocsReviewData, |
| 6 | +} from './types'; |
2 | 7 |
|
3 | | -const fixtureModules = import.meta.glob('./fixture.json', { eager: true }) as Record< |
| 8 | +const FIXTURE_LOG_PREFIX = '[google-docs][fixture]'; |
| 9 | + |
| 10 | +const fixtureModules = import.meta.glob('./*.json', { eager: true }) as Record< |
4 | 11 | string, |
5 | 12 | { default: unknown } |
6 | 13 | >; |
7 | 14 |
|
8 | | -const hasRequiredShape = (value: unknown): value is GoogleDocsReviewFixture => { |
9 | | - if (!value || typeof value !== 'object') { |
10 | | - return false; |
11 | | - } |
| 15 | +function isRecord(value: unknown): value is Record<string, unknown> { |
| 16 | + return typeof value === 'object' && value !== null && !Array.isArray(value); |
| 17 | +} |
| 18 | + |
| 19 | +function isNormalizedDocumentLike(value: unknown): value is ReviewNormalizedDocument { |
| 20 | + return isRecord(value) && Array.isArray(value.contentBlocks) && Array.isArray(value.tables); |
| 21 | +} |
| 22 | + |
| 23 | +function isEntryBlockGraphLike(value: unknown): value is ReviewEntryBlockGraph { |
| 24 | + return isRecord(value) && Array.isArray(value.entries) && Array.isArray(value.excludedSourceRefs); |
| 25 | +} |
12 | 26 |
|
13 | | - const candidate = value as Record<string, unknown>; |
| 27 | +function hasReviewFixtureShape(value: unknown): value is GoogleDocsReviewData { |
14 | 28 | return ( |
15 | | - Array.isArray(candidate.entries) && |
16 | | - Array.isArray(candidate.assets) && |
17 | | - !!candidate.normalizedDocument && |
18 | | - typeof candidate.normalizedDocument === 'object' && |
19 | | - !!candidate.mappingPlan && |
20 | | - typeof candidate.mappingPlan === 'object' |
| 29 | + isRecord(value) && |
| 30 | + Array.isArray(value.entries) && |
| 31 | + Array.isArray(value.assets) && |
| 32 | + isNormalizedDocumentLike(value.originalNormalizedDocument) && |
| 33 | + isNormalizedDocumentLike(value.editableNormalizedDocument) && |
| 34 | + isEntryBlockGraphLike(value.entryBlockGraph) |
21 | 35 | ); |
22 | | -}; |
| 36 | +} |
| 37 | + |
| 38 | +function hasNormalizedDocumentPayloadShape(value: unknown): value is { |
| 39 | + normalizedDocument: ReviewNormalizedDocument; |
| 40 | + entryBlockGraph: ReviewEntryBlockGraph; |
| 41 | + contentTypes?: Array<Record<string, unknown>>; |
| 42 | + assets?: ReviewAsset[]; |
| 43 | + cmaAssets?: ReviewAsset[]; |
| 44 | + referenceGraph?: GoogleDocsReviewData['referenceGraph']; |
| 45 | +} { |
| 46 | + return ( |
| 47 | + isRecord(value) && |
| 48 | + isNormalizedDocumentLike(value.normalizedDocument) && |
| 49 | + isEntryBlockGraphLike(value.entryBlockGraph) |
| 50 | + ); |
| 51 | +} |
| 52 | + |
| 53 | +function buildEntriesFromEntryBlockGraph( |
| 54 | + entryBlockGraph: ReviewEntryBlockGraph, |
| 55 | + contentTypes: Array<Record<string, unknown>> = [] |
| 56 | +): GoogleDocsReviewData['entries'] { |
| 57 | + const contentTypeNameById = new Map<string, string>(); |
| 58 | + |
| 59 | + contentTypes.forEach((contentType) => { |
| 60 | + const sys = isRecord(contentType.sys) ? contentType.sys : null; |
| 61 | + const contentTypeId = typeof sys?.id === 'string' ? sys.id : null; |
| 62 | + const contentTypeName = typeof contentType.name === 'string' ? contentType.name : null; |
| 63 | + |
| 64 | + if (contentTypeId && contentTypeName) { |
| 65 | + contentTypeNameById.set(contentTypeId, contentTypeName); |
| 66 | + } |
| 67 | + }); |
| 68 | + |
| 69 | + return entryBlockGraph.entries.map((entry, index) => ({ |
| 70 | + tempId: entry.tempId, |
| 71 | + contentTypeId: entry.contentTypeId, |
| 72 | + fields: { |
| 73 | + title: { |
| 74 | + 'en-US': |
| 75 | + contentTypeNameById.get(entry.contentTypeId) ?? |
| 76 | + entry.tempId ?? |
| 77 | + `${entry.contentTypeId}-${index + 1}`, |
| 78 | + }, |
| 79 | + }, |
| 80 | + })); |
| 81 | +} |
| 82 | + |
| 83 | +export function coerceGoogleDocsReviewData(value: unknown): GoogleDocsReviewData | null { |
| 84 | + if (hasReviewFixtureShape(value)) { |
| 85 | + return value; |
| 86 | + } |
| 87 | + |
| 88 | + if (hasNormalizedDocumentPayloadShape(value)) { |
| 89 | + const assets = Array.isArray(value.assets) |
| 90 | + ? value.assets |
| 91 | + : Array.isArray(value.cmaAssets) |
| 92 | + ? value.cmaAssets |
| 93 | + : []; |
| 94 | + |
| 95 | + return { |
| 96 | + entries: buildEntriesFromEntryBlockGraph(value.entryBlockGraph, value.contentTypes ?? []), |
| 97 | + assets, |
| 98 | + referenceGraph: isRecord(value.referenceGraph) ? value.referenceGraph : undefined, |
| 99 | + originalNormalizedDocument: value.normalizedDocument, |
| 100 | + editableNormalizedDocument: structuredClone(value.normalizedDocument), |
| 101 | + entryBlockGraph: value.entryBlockGraph, |
| 102 | + }; |
| 103 | + } |
| 104 | + |
| 105 | + return null; |
| 106 | +} |
| 107 | + |
| 108 | +export const loadGoogleDocsReviewData = (): GoogleDocsReviewData | null => { |
| 109 | + const availableFixtureFiles = Object.keys(fixtureModules) |
| 110 | + .map((path) => path.replace('./', '')) |
| 111 | + .sort(); |
| 112 | + |
| 113 | + console.info(FIXTURE_LOG_PREFIX, 'Attempting to load review fixture.', { |
| 114 | + requestedFile: 'fixture.json', |
| 115 | + availableFixtureFiles, |
| 116 | + }); |
23 | 117 |
|
24 | | -export const loadGoogleDocsReviewFixture = (): GoogleDocsReviewFixture | null => { |
25 | 118 | const module = fixtureModules['./fixture.json']; |
26 | 119 | if (!module) { |
| 120 | + console.warn( |
| 121 | + FIXTURE_LOG_PREFIX, |
| 122 | + 'Unable to find fixture.json. Add src/fixtures/googleDocsReview/fixture.json or rename the fixture you want to load.' |
| 123 | + ); |
| 124 | + return null; |
| 125 | + } |
| 126 | + |
| 127 | + const fixture = coerceGoogleDocsReviewData(module.default); |
| 128 | + if (!fixture) { |
| 129 | + console.warn( |
| 130 | + FIXTURE_LOG_PREFIX, |
| 131 | + 'fixture.json was found but does not match a supported Google Docs review fixture shape.', |
| 132 | + { |
| 133 | + topLevelKeys: |
| 134 | + module.default && typeof module.default === 'object' |
| 135 | + ? Object.keys(module.default as Record<string, unknown>).sort() |
| 136 | + : [], |
| 137 | + } |
| 138 | + ); |
27 | 139 | return null; |
28 | 140 | } |
29 | 141 |
|
30 | | - return hasRequiredShape(module.default) ? module.default : null; |
| 142 | + const variant = hasReviewFixtureShape(module.default) |
| 143 | + ? 'review-fixture' |
| 144 | + : hasNormalizedDocumentPayloadShape(module.default) |
| 145 | + ? 'normalized-document-payload' |
| 146 | + : 'unknown'; |
| 147 | + |
| 148 | + console.info(FIXTURE_LOG_PREFIX, 'Review fixture loaded successfully.', { |
| 149 | + variant, |
| 150 | + entries: fixture.entries.length, |
| 151 | + assets: fixture.assets.length, |
| 152 | + contentBlocks: fixture.originalNormalizedDocument.contentBlocks.length, |
| 153 | + tables: fixture.originalNormalizedDocument.tables.length, |
| 154 | + }); |
| 155 | + |
| 156 | + return fixture; |
31 | 157 | }; |
0 commit comments