Skip to content

Commit

Permalink
feat: add createMigration and createWriteClient (#350)
Browse files Browse the repository at this point in the history
* refactor: extract `BaseClient` from `Client`

* feat: `WriteClient`

* feat: `Migration`

* test: type test migration types

* refactor: `MigrationPrismicDocumentParams`

* wip: `client.migrate`

* feat: patch migration document data

* feat: discover existing images automatically

* fix: handle edge cases and log API responses properly

* fix: use `JSON.parse` instead of `JSON.stringify`

* docs: document new code

* docs: document new warning messages

* feat: validate assets metadata

* refactor: skip initial delay on `pLimit` when possible

* test: `Client` updated constructor

* test: `Migration` class

* test: `WriteClient` constructor

* test: setup mocks for asset API and migration API

* style: lint

* test: `WriteClient` core methods

* chore(deps): bump `@prismicio/mock`

* fix tests on old node version

* test: skip tests on specific node version

* style: unused variable

* refactor: `WriteClient.migrate` reporter

* refactor: use more predictable asset API in tests

* test: `WriteClient.migrateCreateAssets`

* test: `WriteClient.migrateCreateDocuments`

* test: ensure unique repository name for all tests

* test: `WriteClient.fetchForeignAsset`

* test: `WriteClient.migrateUpdateDocuments` (WIP)

* test: `WriteClient.migrateUpdateDocuments`

* test: `pLimit`

* test: skip `WriteClient.fetchForeignAsset` tests on Node 18

* refactor: per self review

* refactor: use random API key pool for migration API demo

* docs: capitalize `Migration API`

* docs: messages copy editing

* chore: ignore all `.tgz` files

* refactor: `apiEndpoint` to `documentAPIEndpoint`

Co-authored-by: Angelo Ashmore <[email protected]>

* docs: wording

Co-authored-by: Angelo Ashmore <[email protected]>

* refactor: per review (API, wording, tests)

Co-authored-by: Angelo Ashmore <[email protected]>

* fix: stronger `is.richText` helper

* refactor: per review (API, tests)

* refactor: use classes to detect migration field

* chore: rename file casing

* refactor: rename `_prepare` to `_resolve`

* refactor: abstract thunks under `Migration#createContentRelationship`

* docs: document new types

* refactor: simplify migration documents traversing

* refactor: remove documents and assets maps

* refactor: remove existing assets querying

* refactor: remove `dependencies` concept

* refactor: remove `MigrationContentRelationship` class

* refactor: remove all child asset classes

* refactor: remove `addThumbnail` method

* refactor: tests naming

* refactor: per review

* fix: allow not updating document's title

* fix: missing migration embed and slice types

* docs: add `migrate` example

---------

Co-authored-by: lihbr <[email protected]>
Co-authored-by: Angelo Ashmore <[email protected]>
  • Loading branch information
3 people authored Sep 23, 2024
1 parent 088f9c5 commit 7dc2950
Show file tree
Hide file tree
Showing 93 changed files with 15,270 additions and 2,078 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ CHANGELOG.md
# .gitignore copy

# custom
*.tgz
dist
examples/**/package-lock.json

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# custom
*.tgz
dist
examples/**/package-lock.json

Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG.md
# .gitignore copy

# custom
*.tgz
dist
examples/**/package-lock.json

Expand Down
26 changes: 26 additions & 0 deletions examples/migrate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Migrate

This example shows a migration script leveraging `@prismicio/client`'s write client
and the migration helper to migrate existing content to Prismic. `migrate.ts` is the
TypeScript exact equivalent of `migrate.mjs`.

Learn more about migrating to Prismic on the [migration documentation](https://prismic.io/docs/migration).

## How to run the example

> Scripts in this example uses an hypothetical WordPress client, therefore, they are not runnable as-is.
```sh
# Clone the repository to your computer
git clone https://github.com/prismicio/prismic-client.git
cd prismic-client/examples/migrate

# Install the dependencies
npm install

# Run the example (TypeScript)
npx tsx migrate.ts

# Run the example (JavaScript)
node migrate.mjs
```
101 changes: 101 additions & 0 deletions examples/migrate/migrate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as prismic from "@prismicio/client"
import { htmlAsRichText } from "@prismicio/migrate"
import "dotenv/config"
// An hypothetical WordPress client
import { createWordPressClient } from "wordpress"

import { repositoryName } from "./slicemachine.config.json"

// Prismic setup
const writeClient = prismic.createWriteClient(repositoryName, {
writeToken: process.env.PRISMIC_WRITE_TOKEN,
})

const migration = prismic.createMigration()

// Custom migration script logic

const convertWPDocument = (wpDocument) => {
switch (wpDocument.type) {
case "page":
return convertWPPage(wpDocument)
case "settings":
return convertWPSettings(wpDocument)
}

throw new Error(`Unsupported document type: ${wpDocument.type}`)
}

const convertWPPage = (wpPage) => {
return migration.createDocument(
{
type: "page",
lang: wpPage.lang,
uid: wpPage.slug,
tags: ["wordpress"],
data: {
meta_title: wpPage.meta_title,
meta_description: wpPage.meta_description,
meta_image: migration.createAsset(
wpPage.meta_image.url,
wpPage.meta_image.name,
),
title: wpHTMLAsRichText(wpPage.title),
body: wpHTMLAsRichText(wpPage.content),
},
},
wpPage.name,
{
masterLanguageDocument: () =>
migration.getByUID(
wpPage.masterLanguageDocument.type,
wpPage.masterLanguageDocument.uid,
),
},
)
}

const convertWPSettings = (wpSettings) => {
return migration.createDocument(
{
type: "settings",
lang: wpSettings.lang,
tags: ["wordpress"],
data: {
title: wpHTMLAsRichText(wpSettings.name),
},
},
"Settings",
)
}

const wpHTMLAsRichText = (html) => {
return htmlAsRichText(html, {
serializer: {
img: ({ node }) => {
const src = node.properties.src
const filename = src.split("/").pop()
const alt = node.properties.alt

return {
type: "image",
id: migration.createAsset(src, filename, { alt }),
}
},
},
}).result
}

// Fetching and converting WordPress documents
const wpClient = createWordPressClient("https://example.com/wp-json")

const wpDocuments = await wpClient.dangerouslyGetAllDocuments()

for (const wpDocument of wpDocuments) {
convertWPDocument(wpDocument)
}

// Execute the prepared migration at the very end of the script
await writeClient.migrate(migration, {
reporter: (event) => console.info(event),
})
102 changes: 102 additions & 0 deletions examples/migrate/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as prismic from "@prismicio/client"
import { htmlAsRichText } from "@prismicio/migrate"
import "dotenv/config"
// An hypothetical WordPress client
// @ts-expect-error - This is an hypothetical WordPress client
import { type WPDocument, createWordPressClient } from "wordpress"

import { repositoryName } from "./slicemachine.config.json"

// Prismic setup
const writeClient = prismic.createWriteClient(repositoryName, {
writeToken: process.env.PRISMIC_WRITE_TOKEN!,
})

const migration = prismic.createMigration()

// Custom migration script logic

const convertWPDocument = (wpDocument: WPDocument) => {
switch (wpDocument.type) {
case "page":
return convertWPPage(wpDocument)
case "settings":
return convertWPSettings(wpDocument)
}

throw new Error(`Unsupported document type: ${wpDocument.type}`)
}

const convertWPPage = (wpPage: WPDocument) => {
return migration.createDocument(
{
type: "page",
lang: wpPage.lang,
uid: wpPage.slug,
tags: ["wordpress"],
data: {
meta_title: wpPage.meta_title,
meta_description: wpPage.meta_description,
meta_image: migration.createAsset(
wpPage.meta_image.url,
wpPage.meta_image.name,
),
title: wpHTMLAsRichText(wpPage.title),
body: wpHTMLAsRichText(wpPage.content),
},
},
wpPage.name,
{
masterLanguageDocument: () =>
migration.getByUID(
wpPage.masterLanguageDocument.type,
wpPage.masterLanguageDocument.uid,
),
},
)
}

const convertWPSettings = (wpSettings: WPDocument) => {
return migration.createDocument(
{
type: "settings",
lang: wpSettings.lang,
tags: ["wordpress"],
data: {
title: wpHTMLAsRichText(wpSettings.name),
},
},
"Settings",
)
}

const wpHTMLAsRichText = (html: string) => {
return htmlAsRichText(html, {
serializer: {
img: ({ node }) => {
const src = node.properties.src as string
const filename = src.split("/").pop()!
const alt = node.properties.alt as string

return {
type: "image",
id: migration.createAsset(src, filename, { alt }),
}
},
},
}).result
}

// Fetching and converting WordPress documents
const wpClient = createWordPressClient("https://example.com/wp-json")

const wpDocuments = await wpClient.dangerouslyGetAllDocuments()

for (const wpDocument of wpDocuments) {
convertWPDocument(wpDocument)
}

// Execute the prepared migration at the very end of the script
await writeClient.migrate(migration, {
reporter: (event) => console.info(event),
})
11 changes: 11 additions & 0 deletions examples/migrate/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"type": "module",
"dependencies": {
"@prismicio/client": "latest",
"@prismicio/migrate": "latest"
},
"devDependencies": {
"tsx": "^4.19.1",
"typescript": "^5.6.2"
}
}
6 changes: 6 additions & 0 deletions examples/migrate/slicemachine.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"libraries": ["./slices"],
"adapter": "@slicemachine/adapter-next",
"repositoryName": "qwerty",
"localSliceSimulatorURL": "http://localhost:3000/slice-simulator"
}
14 changes: 14 additions & 0 deletions examples/migrate/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"strict": true,
"strictNullChecks": true,
"esModuleInterop": true,
"skipDefaultLibCheck": true,
"skipLibCheck": true
}
}
23 changes: 23 additions & 0 deletions messages/avoid-write-client-in-browser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Avoid write client in browser

`@prismicio/client`'s write client uses credentials to authenticate write queries to a Prismic repository.

The repository write token and Migration API key must be provided when creating a `@prismicio/client` write client like the following:

```typescript
import * as prismic from "@prismicio/client";

const writeClient = prismic.createWriteClient("example-prismic-repo", {
writeToken: "xxx"
})
```

If the write client is exposed to the browser, so are its tokens. Malicious actors will have write access to your repository.

Use the non-write client when write actions are not needed. The non-write client only has read access to the repository and can safely be used in the browser. Be aware the client's access token, if used, will be exposed in the browser.

```typescript
import * as prismic from "@prismicio/client";

const client = prismic.createClient("example-prismic-repo")
```
10 changes: 3 additions & 7 deletions messages/endpoint-must-use-cdn.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
# `endpoint` must use CDN

`@prismicio/client` uses either a Prismic repository name or a Prismic Rest API v2 repository endpoint to query content from Prismic.
`@prismicio/client` uses either a Prismic repository name or a repository-specific Document API endpoint to query content from Prismic.

The repository name or repository endpoint must be provided when creating a `@prismicio/client` like the following:
The repository name must be provided when creating a `@prismicio/client` like the following:

```typescript
import * as prismic from "@prismicio/client";

// Using the repository name
const client = prismic.createClient("example-prismic-repo")

// Using the repository endpoint
const client = prismic.createClient("https://example-prismic-repo.cdn.prismic.io/api/v2")
```

When creating a `@prismicio/client` with a repository endpoint, the endpoint's subdomain must feature the `.cdn` suffix.
When creating a `@prismicio/client` with a repository endpoint (not recommended), the endpoint's subdomain must feature the `.cdn` suffix.

```typescript
import * as prismic from "@prismicio/client";
Expand Down
29 changes: 29 additions & 0 deletions messages/prefer-repository-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Prefer repository name

`@prismicio/client` uses either a Prismic repository name or a repository-specific Document API endpoint to query content from Prismic.

The repository name must be provided when creating a `@prismicio/client` like the following:

```typescript
import * as prismic from "@prismicio/client";

const client = prismic.createClient("example-prismic-repo")
```

When proxying a Prismic API v2 repository endpoint (not recommended), the `documentAPIEndpoint` option can be used to specify that endpoint.

```typescript
import * as prismic from "@prismicio/client"

// ✅ Correct
const client = prismic.createClient("my-repo-name", {
documentAPIEndpoint: "https://example.com/my-prismic-proxy"
})

// ❌ Incorrect: repository name can't be inferred from a proxied endpoint
const client = prismic.createClient("https://example.com/my-prismic-proxy", {
documentAPIEndpoint: "https://example.com/my-prismic-proxy"
})
```

Proxying a Prismic API v2 repository endpoint can have unexpected side-effects and cause performance issues when querying Prismic.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"imgix-url-builder": "^0.0.5"
},
"devDependencies": {
"@prismicio/mock": "^0.3.7",
"@prismicio/mock": "^0.3.9",
"@prismicio/types-internal": "^2.6.0",
"@size-limit/preset-small-lib": "^11.1.4",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
Expand Down
Loading

0 comments on commit 7dc2950

Please sign in to comment.