-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add
createWriteClient
and createMigration
- Loading branch information
1 parent
601edd5
commit 58c29b2
Showing
3 changed files
with
303 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
import type { | ||
ContentRelationshipField, | ||
FilledContentRelationshipField, | ||
} from "./types/value/contentRelationship" | ||
import type { PrismicDocument } from "./types/value/document" | ||
import type { LinkField } from "./types/value/link" | ||
|
||
type PendingPrismicDocument<TDocument extends PrismicDocument> = | ||
InjectMigrationSpecificTypes< | ||
Pick<TDocument, "type" | "uid" | "lang" | "tags" | "data"> | ||
> | ||
|
||
type ExistingPrismicDocument<TDocument extends PrismicDocument> = | ||
PendingPrismicDocument<TDocument> & Pick<TDocument, "id"> | ||
|
||
type MigrationDocument<TDocument extends PrismicDocument> = | ||
| PendingPrismicDocument<TDocument> | ||
| ExistingPrismicDocument<TDocument> | ||
|
||
type MigrationContentRelationship = | ||
| PrismicDocument | ||
| PrismicMigrationDocument | ||
| undefined | ||
| (() => PrismicDocument | PrismicMigrationDocument | undefined) | ||
| (() => Promise<PrismicDocument | PrismicMigrationDocument | undefined>) | ||
|
||
type MigrationFilledContentRelationshipField = Pick< | ||
FilledContentRelationshipField, | ||
"link_type" | "id" | ||
> | ||
|
||
type InjectMigrationSpecificTypes<T> = T extends null | ||
? undefined | ||
: T extends LinkField | ContentRelationshipField | ||
? T | MigrationContentRelationship | ||
: // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
T extends Record<any, any> | ||
? { [P in keyof T]: InjectMigrationSpecificTypes<T[P]> } | ||
: T extends Array<infer U> | ||
? Array<InjectMigrationSpecificTypes<U>> | ||
: T | ||
|
||
export const createMigration = < | ||
TDocument extends PrismicDocument, | ||
>(): Migration<TDocument> => new Migration<TDocument>() | ||
|
||
export class Migration<TDocument extends PrismicDocument> { | ||
documents: PrismicMigrationDocument<TDocument>[] = [] | ||
|
||
createDocument( | ||
document: PendingPrismicDocument<TDocument>, | ||
title: string, | ||
options?: { | ||
masterLanguageDocument?: MigrationContentRelationship | ||
}, | ||
): PrismicMigrationDocument<TDocument> { | ||
const migrationDocument = new PrismicMigrationDocument( | ||
document, | ||
title, | ||
options, | ||
) | ||
|
||
this.documents.push(migrationDocument) | ||
|
||
return migrationDocument | ||
} | ||
|
||
updateDocument( | ||
document: ExistingPrismicDocument<TDocument>, | ||
title?: string, | ||
options?: { | ||
masterLanguageDocument?: MigrationContentRelationship | ||
}, | ||
): PrismicMigrationDocument<TDocument> { | ||
const migrationDocument = new PrismicMigrationDocument( | ||
document, | ||
title, | ||
options, | ||
) | ||
|
||
this.documents.push(migrationDocument) | ||
|
||
return migrationDocument | ||
} | ||
|
||
getByUID<TType extends TDocument["type"]>( | ||
type: TType, | ||
uid: string, | ||
): PrismicMigrationDocument<Extract<TDocument, { type: TType }>> | undefined { | ||
return this.documents.find( | ||
( | ||
doc, | ||
): doc is PrismicMigrationDocument<Extract<TDocument, { type: TType }>> => | ||
doc.document.type === type && doc.document.uid === uid, | ||
) | ||
} | ||
|
||
getSingle( | ||
type: TDocument["type"], | ||
): PrismicMigrationDocument<TDocument> | undefined { | ||
return this.documents.find((doc) => doc.document.type === type) | ||
} | ||
} | ||
|
||
export class PrismicMigrationDocument< | ||
TDocument extends PrismicDocument = PrismicDocument, | ||
> { | ||
document: MigrationDocument<TDocument> & Partial<Pick<TDocument, "id">> | ||
title?: string | ||
masterLanguageDocument?: MigrationContentRelationship | ||
|
||
constructor( | ||
document: MigrationDocument<TDocument>, | ||
title?: string, | ||
options?: { | ||
masterLanguageDocument?: MigrationContentRelationship | ||
}, | ||
) { | ||
this.document = document | ||
this.title = title | ||
this.masterLanguageDocument = options?.masterLanguageDocument | ||
} | ||
} | ||
|
||
export async function resolveMigrationDocumentData( | ||
input: unknown, | ||
): Promise<unknown> { | ||
if (input instanceof PrismicMigrationDocument || isPrismicDocument(input)) { | ||
return resolveMigrationContentRelationship(input) | ||
} | ||
|
||
if (typeof input === "function") { | ||
return await resolveMigrationDocumentData(await input()) | ||
} | ||
|
||
if (Array.isArray(input)) { | ||
return await Promise.all( | ||
input.map(async (element) => await resolveMigrationDocumentData(element)), | ||
) | ||
} | ||
|
||
if (typeof input === "object") { | ||
const res: Record<PropertyKey, unknown> = {} | ||
|
||
for (const key in input) { | ||
res[key] = await resolveMigrationDocumentData( | ||
input[key as keyof typeof input], | ||
) | ||
} | ||
|
||
return res | ||
} | ||
|
||
return input | ||
} | ||
|
||
export async function resolveMigrationContentRelationship( | ||
relation: MigrationContentRelationship, | ||
): Promise<MigrationFilledContentRelationshipField | undefined> { | ||
if (typeof relation === "function") { | ||
return resolveMigrationContentRelationship(await relation()) | ||
} | ||
|
||
if (relation instanceof PrismicMigrationDocument) { | ||
return relation.document.id | ||
? { link_type: "Document", id: relation.document.id } | ||
: undefined | ||
} | ||
|
||
if (relation) { | ||
return { link_type: "Document", id: relation.id } | ||
} | ||
} | ||
|
||
function isPrismicDocument(input: unknown): input is PrismicDocument { | ||
try { | ||
return ( | ||
typeof input === "object" && | ||
input !== null && | ||
"id" in input && | ||
"href" in input && | ||
typeof input.href === "string" && | ||
new URL(input.href).host.endsWith(".prismic.io") | ||
) | ||
} catch { | ||
return false | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import type { PrismicDocument } from "./types/value/document" | ||
|
||
import { Client, type ClientConfig } from "./createClient" | ||
import { | ||
type Migration, | ||
type PrismicMigrationDocument, | ||
resolveMigrationContentRelationship, | ||
resolveMigrationDocumentData, | ||
} from "./createMigration" | ||
import { getRepositoryName } from "./getRepositoryName" | ||
|
||
export const createWriteClient = <TDocuments extends PrismicDocument>( | ||
repositoryName: string, | ||
config: ClientConfig & { writeToken: string }, | ||
): WriteClient<TDocuments> => | ||
new WriteClient<TDocuments>(repositoryName, config) | ||
|
||
export class WriteClient< | ||
TDocument extends PrismicDocument = PrismicDocument, | ||
> extends Client<TDocument> { | ||
writeToken: string | ||
migrationAPIEndpoint = "https://migration.prismic.io" | ||
|
||
constructor( | ||
repositoryName: string, | ||
config: ClientConfig & { writeToken: string }, | ||
) { | ||
super(repositoryName) | ||
this.writeToken = config.writeToken | ||
} | ||
|
||
async migrate(migration: Migration<TDocument>): Promise<void> { | ||
const docsWithoutMasterLangDoc = migration.documents.filter( | ||
(doc) => !doc.masterLanguageDocument, | ||
) | ||
const docsWithMasterLangDoc = migration.documents.filter( | ||
(doc) => doc.masterLanguageDocument, | ||
) | ||
|
||
// 1. Upload all docs without a master lang with no data | ||
for (const doc of docsWithoutMasterLangDoc) { | ||
await this.#pushDocument(doc, { withData: false }) | ||
} | ||
|
||
// 2. Upload all documents with a master lang with no data | ||
for (const doc of docsWithMasterLangDoc) { | ||
await this.#pushDocument(doc, { withData: false }) | ||
} | ||
|
||
// 3. Upload all documents with resolved data | ||
for (const doc of migration.documents) { | ||
await this.#pushDocument(doc) | ||
} | ||
} | ||
|
||
async #pushDocument( | ||
doc: PrismicMigrationDocument<TDocument>, | ||
options?: { withData?: boolean }, | ||
) { | ||
const isUpdating = Boolean(doc.document.id) | ||
|
||
const documentData = | ||
(options?.withData ?? true) | ||
? await resolveMigrationDocumentData(doc.document.data) | ||
: {} | ||
const alternateLanguageDocument = await resolveMigrationContentRelationship( | ||
doc.masterLanguageDocument, | ||
) | ||
|
||
const body = { | ||
...doc.document, | ||
data: documentData, | ||
title: doc.title, | ||
alternate_language_id: alternateLanguageDocument?.id, | ||
} | ||
|
||
const response = await this.#fetch( | ||
new URL( | ||
isUpdating ? `documents/${doc.document.id}` : "documents", | ||
this.migrationAPIEndpoint, | ||
), | ||
{ method: isUpdating ? "PUT" : "POST", body: JSON.stringify(body) }, | ||
) | ||
if (!response.ok) { | ||
throw new Error(`Failed to push document: ${await response.text()}`) | ||
} | ||
|
||
const responseBody = await response.json() | ||
doc.document.id = responseBody.id | ||
|
||
// Wait for API rate limiting | ||
await new Promise((res) => setTimeout(res, 1500)) | ||
|
||
return responseBody | ||
} | ||
|
||
async #fetch(input: RequestInfo | URL, init?: RequestInit) { | ||
const request = new Request(input, init) | ||
|
||
const headers = new Headers(init?.headers) | ||
headers.set("repository", getRepositoryName(this.endpoint)) | ||
headers.set("authorization", `Bearer ${this.writeToken}`) | ||
headers.set("x-api-key", "g2DA3EKWvx8uxVYcNFrmT5nJpon1Vi9V4XcOibJD") | ||
headers.set("content-type", "application/json") | ||
|
||
return fetch(new Request(request, { headers })) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters