@farm.js/strapi 0.1.1-beta.4
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.
- package/LICENSE +22 -0
- package/README.md +173 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +14 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +46 -0
- package/dist/image.d.ts +35 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +64 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/query.d.ts +14 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +44 -0
- package/dist/webhook.d.ts +12 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +76 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Farm.js Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @farm.js/strapi
|
|
2
|
+
|
|
3
|
+
Strapi 5 CMS integration for Farm.js applications. It configures the official Strapi client,
|
|
4
|
+
unwraps typed collection reads, builds responsive image props from generated upload variants, and
|
|
5
|
+
receives authenticated webhooks so cached content can be refreshed after an editor publishes.
|
|
6
|
+
|
|
7
|
+
See [Stability and Support](https://farmjs.dev/docs/stability) for what this package guarantees.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @farm.js/strapi @strapi/client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`@strapi/client` is a peer dependency so the application and integration use one client version.
|
|
16
|
+
|
|
17
|
+
## Configure
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { defineConfig } from "@farm.js/core";
|
|
21
|
+
import { strapi } from "@farm.js/strapi";
|
|
22
|
+
|
|
23
|
+
export default defineConfig({
|
|
24
|
+
integrations: {
|
|
25
|
+
cms: strapi(),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
With no options, values come from the environment:
|
|
31
|
+
|
|
32
|
+
| Variable | Purpose |
|
|
33
|
+
| ----------------------- | --------------------------------------------------------------- |
|
|
34
|
+
| `STRAPI_API_URL` | Strapi Content API base URL, including `/api`. |
|
|
35
|
+
| `STRAPI_MEDIA_URL` | Optional public media origin. Defaults to the API URL's origin. |
|
|
36
|
+
| `STRAPI_API_TOKEN` | Optional server-only API token for non-public content. |
|
|
37
|
+
| `STRAPI_WEBHOOK_SECRET` | Required once webhook invalidation is configured. |
|
|
38
|
+
|
|
39
|
+
A missing API URL fails while configuration loads instead of on the first content request.
|
|
40
|
+
|
|
41
|
+
## Read content
|
|
42
|
+
|
|
43
|
+
Build the client once and reuse it in the integration and application server code.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// src/lib/cms.server.ts
|
|
47
|
+
import {
|
|
48
|
+
createStrapiClient,
|
|
49
|
+
createStrapiCollection,
|
|
50
|
+
resolveStrapiConfig,
|
|
51
|
+
type StrapiDocument,
|
|
52
|
+
} from "@farm.js/strapi";
|
|
53
|
+
|
|
54
|
+
interface Article extends StrapiDocument {
|
|
55
|
+
title: string;
|
|
56
|
+
slug: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const cms = createStrapiClient(resolveStrapiConfig({}));
|
|
60
|
+
export const articles = createStrapiCollection<Article>(cms, "articles");
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// farm.config.ts
|
|
65
|
+
integrations: {
|
|
66
|
+
cms: strapi({ instance: cms }),
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Wrap collection reads in `createServerQuery` to validate and cache the result. The helper accepts
|
|
71
|
+
the official client's `populate`, `fields`, `filters`, `sort`, `pagination`, `locale`, and `status`
|
|
72
|
+
query options and unwraps the REST `data` envelope.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { createServerQuery } from "@farm.js/core";
|
|
76
|
+
import { articles } from "./cms.server";
|
|
77
|
+
|
|
78
|
+
export const postsQuery = createServerQuery({
|
|
79
|
+
key: () => ["strapi", "articles"],
|
|
80
|
+
staleTime: "5m",
|
|
81
|
+
handler: () =>
|
|
82
|
+
articles.find({
|
|
83
|
+
fields: ["title", "slug"],
|
|
84
|
+
populate: ["cover"],
|
|
85
|
+
sort: ["publishedAt:desc"],
|
|
86
|
+
status: "published",
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The generic describes the fields your application expects. Keep runtime validation on the server
|
|
92
|
+
query boundary because Strapi's returned fields still depend on permissions and `populate`.
|
|
93
|
+
|
|
94
|
+
## Invalidate on publish
|
|
95
|
+
|
|
96
|
+
Strapi can send a fixed header with every webhook. Farm verifies that shared secret before parsing
|
|
97
|
+
the body and asks the application which cache entries the event affects.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
strapi({
|
|
101
|
+
instance: cms,
|
|
102
|
+
webhook: {
|
|
103
|
+
onChange(payload) {
|
|
104
|
+
if (payload.model !== "article") return;
|
|
105
|
+
const entry = payload.entry as { documentId?: string; slug?: string };
|
|
106
|
+
return {
|
|
107
|
+
keys: [
|
|
108
|
+
["strapi", "articles"],
|
|
109
|
+
...(entry.documentId ? [["strapi", "article", entry.documentId] as const] : []),
|
|
110
|
+
],
|
|
111
|
+
paths: ["/articles", ...(entry.slug ? [`/articles/${entry.slug}`] : [])],
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
In Strapi, create a webhook under **Settings > Webhooks**:
|
|
119
|
+
|
|
120
|
+
- URL: `https://your-app.example/api/strapi/webhook`
|
|
121
|
+
- Events: the entry and media events your mapping handles
|
|
122
|
+
- Header name: `x-farm-webhook-secret`
|
|
123
|
+
- Header value: the value of `STRAPI_WEBHOOK_SECRET`
|
|
124
|
+
|
|
125
|
+
You can change both `webhook.path` and `webhook.secretHeader`. Strapi does not sign or retry webhook
|
|
126
|
+
deliveries, so use `staleTime` as the freshness fallback rather than relying on the webhook alone.
|
|
127
|
+
|
|
128
|
+
## Serve images
|
|
129
|
+
|
|
130
|
+
`getStrapiImageProps` selects the smallest generated variant at least as wide as requested and
|
|
131
|
+
builds a `srcset` from the real variants on that asset. It never invents a transformation URL.
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import { getStrapiImageProps, type StrapiMediaAsset } from "@farm.js/strapi";
|
|
135
|
+
|
|
136
|
+
export function Cover({ asset }: { asset: StrapiMediaAsset }) {
|
|
137
|
+
return (
|
|
138
|
+
<img
|
|
139
|
+
{...getStrapiImageProps(asset, {
|
|
140
|
+
mediaUrl: process.env.STRAPI_MEDIA_URL ?? process.env.STRAPI_API_URL,
|
|
141
|
+
width: 800,
|
|
142
|
+
sizes: "100vw",
|
|
143
|
+
})}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Absolute upload-provider URLs are preserved. Relative URLs are resolved against `mediaUrl`, which
|
|
150
|
+
can differ from the Content API host.
|
|
151
|
+
|
|
152
|
+
## Production notes
|
|
153
|
+
|
|
154
|
+
- Keep `STRAPI_API_TOKEN` and the official client in server-only modules. Do not import them from a
|
|
155
|
+
browser entry.
|
|
156
|
+
- Farm's default data cache is in memory per process. Configure a distributed cache adapter when a
|
|
157
|
+
webhook must invalidate every server instance.
|
|
158
|
+
- Strapi 5's draft status requires an API token that can read the content type. Do not make draft
|
|
159
|
+
selection depend on an untrusted query parameter; gate it behind application-owned preview auth.
|
|
160
|
+
|
|
161
|
+
## Options
|
|
162
|
+
|
|
163
|
+
| Option | Default | Notes |
|
|
164
|
+
| ---------------------- | ----------------------- | -------------------------------------------------- |
|
|
165
|
+
| `apiUrl` | `STRAPI_API_URL` | Content API base URL, including `/api`. |
|
|
166
|
+
| `mediaUrl` | API origin | Public origin for relative upload URLs. |
|
|
167
|
+
| `token` | `STRAPI_API_TOKEN` | Server only. |
|
|
168
|
+
| `instance` | | Existing `StrapiClient`; skips API URL validation. |
|
|
169
|
+
| `webhook.secret` | `STRAPI_WEBHOOK_SECRET` | |
|
|
170
|
+
| `webhook.path` | `/api/strapi/webhook` | |
|
|
171
|
+
| `webhook.secretHeader` | `x-farm-webhook-secret` | Configure the same custom header in Strapi. |
|
|
172
|
+
| `webhook.onChange` | | Maps a payload to `{ keys?, paths? }`. |
|
|
173
|
+
| `log` | | Integration lifecycle logger. |
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAExD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,oBAAoB,EAC5B,QAAQ,CAAC,EAAE,YAAY,GACtB,YAAY,CAcd"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { strapi as createClient } from "@strapi/client";
|
|
2
|
+
export function createStrapiClient(config, instance) {
|
|
3
|
+
if (instance)
|
|
4
|
+
return instance;
|
|
5
|
+
if (!config.apiUrl) {
|
|
6
|
+
throw new TypeError("Cannot create a Strapi client without an API URL. Set STRAPI_API_URL or pass apiUrl.");
|
|
7
|
+
}
|
|
8
|
+
// Map only client fields so the public media URL and webhook secret can
|
|
9
|
+
// never reach the provider SDK or one of its request interceptors.
|
|
10
|
+
return createClient({
|
|
11
|
+
baseURL: config.apiUrl,
|
|
12
|
+
...(config.token ? { auth: config.token } : {}),
|
|
13
|
+
});
|
|
14
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { FarmIntegrationLogger, RouteDataCacheKey } from "@farm.js/core";
|
|
2
|
+
import type { StrapiClient } from "@strapi/client";
|
|
3
|
+
export declare const DEFAULT_STRAPI_WEBHOOK_PATH = "/api/strapi/webhook";
|
|
4
|
+
export declare const DEFAULT_STRAPI_WEBHOOK_SECRET_HEADER = "x-farm-webhook-secret";
|
|
5
|
+
export interface StrapiWebhookChange {
|
|
6
|
+
/** Server query keys to invalidate, as passed to `createServerQuery`. */
|
|
7
|
+
keys?: readonly RouteDataCacheKey[];
|
|
8
|
+
/** Route paths whose rendered output should be revalidated. */
|
|
9
|
+
paths?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export interface StrapiWebhookOptions {
|
|
12
|
+
/** Defaults to `STRAPI_WEBHOOK_SECRET`. */
|
|
13
|
+
secret?: string;
|
|
14
|
+
/** Defaults to `/api/strapi/webhook`. */
|
|
15
|
+
path?: string;
|
|
16
|
+
/** Defaults to `x-farm-webhook-secret`. */
|
|
17
|
+
secretHeader?: string;
|
|
18
|
+
/** Maps a Strapi webhook payload to the cache entries it affects. */
|
|
19
|
+
onChange(payload: Record<string, unknown>): StrapiWebhookChange | void | Promise<StrapiWebhookChange | void>;
|
|
20
|
+
}
|
|
21
|
+
export interface StrapiIntegrationInput {
|
|
22
|
+
/** Strapi Content API base URL, for example `http://localhost:1337/api`. */
|
|
23
|
+
apiUrl?: string;
|
|
24
|
+
/** Public origin for relative media URLs. Defaults to the API URL's origin. */
|
|
25
|
+
mediaUrl?: string;
|
|
26
|
+
/** Server-only API token. Defaults to `STRAPI_API_TOKEN`. */
|
|
27
|
+
token?: string;
|
|
28
|
+
/** Existing official Strapi client. When provided, Farm does not construct its own. */
|
|
29
|
+
instance?: StrapiClient;
|
|
30
|
+
webhook?: StrapiWebhookOptions;
|
|
31
|
+
log?: FarmIntegrationLogger;
|
|
32
|
+
}
|
|
33
|
+
export interface ResolvedStrapiConfig {
|
|
34
|
+
apiUrl: string;
|
|
35
|
+
mediaUrl: string;
|
|
36
|
+
token?: string;
|
|
37
|
+
webhookSecret?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare function resolveStrapiConfig(input: StrapiIntegrationInput): ResolvedStrapiConfig;
|
|
40
|
+
export declare function strapiIntegrationConfig(resolved: ResolvedStrapiConfig, input: StrapiIntegrationInput): import("@farm.js/core").FarmIntegrationConfigDefinition<ResolvedStrapiConfig, import("@farm.js/core").FarmSchema | undefined>;
|
|
41
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE9E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,eAAO,MAAM,2BAA2B,wBAAwB,CAAC;AACjE,eAAO,MAAM,oCAAoC,0BAA0B,CAAC;AAE5E,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,IAAI,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACpC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,QAAQ,CACN,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,mBAAmB,GAAG,IAAI,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;CACrE;AAED,MAAM,WAAW,sBAAsB;IACrC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,GAAG,CAAC,EAAE,qBAAqB,CAAC;CAC7B;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAsBD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,sBAAsB,GAAG,oBAAoB,CAgBvF;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,oBAAoB,EAC9B,KAAK,EAAE,sBAAsB,iIAU9B"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { integrationConfig } from "@farm.js/integration-utils";
|
|
2
|
+
export const DEFAULT_STRAPI_WEBHOOK_PATH = "/api/strapi/webhook";
|
|
3
|
+
export const DEFAULT_STRAPI_WEBHOOK_SECRET_HEADER = "x-farm-webhook-secret";
|
|
4
|
+
function normalizeUrl(value, label) {
|
|
5
|
+
if (!value)
|
|
6
|
+
return "";
|
|
7
|
+
let url;
|
|
8
|
+
try {
|
|
9
|
+
url = new URL(value);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new TypeError(`${label} must be an absolute HTTP or HTTPS URL`);
|
|
13
|
+
}
|
|
14
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
15
|
+
throw new TypeError(`${label} must use HTTP or HTTPS`);
|
|
16
|
+
}
|
|
17
|
+
return url.toString().replace(/\/$/, "");
|
|
18
|
+
}
|
|
19
|
+
function readEnv(name) {
|
|
20
|
+
return process.env[name] || undefined;
|
|
21
|
+
}
|
|
22
|
+
export function resolveStrapiConfig(input) {
|
|
23
|
+
const configuredMediaUrl = input.mediaUrl ?? readEnv("STRAPI_MEDIA_URL");
|
|
24
|
+
const mediaUrl = normalizeUrl(configuredMediaUrl, "Strapi media URL");
|
|
25
|
+
// A supplied client owns its Content API configuration. Only inspect the
|
|
26
|
+
// API URL when Farm needs it to build a client or derive the media origin.
|
|
27
|
+
const apiUrl = !input.instance || !mediaUrl
|
|
28
|
+
? normalizeUrl(input.apiUrl ?? readEnv("STRAPI_API_URL"), "Strapi API URL")
|
|
29
|
+
: "";
|
|
30
|
+
return {
|
|
31
|
+
apiUrl,
|
|
32
|
+
mediaUrl: mediaUrl || (apiUrl ? new URL(apiUrl).origin : ""),
|
|
33
|
+
token: input.token ?? readEnv("STRAPI_API_TOKEN"),
|
|
34
|
+
webhookSecret: input.webhook?.secret ?? readEnv("STRAPI_WEBHOOK_SECRET"),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function strapiIntegrationConfig(resolved, input) {
|
|
38
|
+
const required = input.instance ? [] : ["apiUrl"];
|
|
39
|
+
if (input.webhook)
|
|
40
|
+
required.push("webhookSecret");
|
|
41
|
+
return integrationConfig({
|
|
42
|
+
label: "Strapi integration",
|
|
43
|
+
input: resolved,
|
|
44
|
+
required,
|
|
45
|
+
});
|
|
46
|
+
}
|
package/dist/image.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface StrapiMediaFormat {
|
|
2
|
+
url: string;
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
}
|
|
6
|
+
export interface StrapiMediaAsset extends StrapiMediaFormat {
|
|
7
|
+
alternativeText?: string | null;
|
|
8
|
+
formats?: Record<string, StrapiMediaFormat | null | undefined> | null;
|
|
9
|
+
}
|
|
10
|
+
export interface StrapiImageOptions {
|
|
11
|
+
/** Public media origin used to resolve relative upload URLs. */
|
|
12
|
+
mediaUrl?: string;
|
|
13
|
+
/** Requested rendered width. Defaults to the original asset width. */
|
|
14
|
+
width?: number;
|
|
15
|
+
alt?: string;
|
|
16
|
+
sizes?: string;
|
|
17
|
+
loading?: "eager" | "lazy";
|
|
18
|
+
}
|
|
19
|
+
export interface StrapiImageProps {
|
|
20
|
+
src: string;
|
|
21
|
+
srcSet: string;
|
|
22
|
+
alt: string;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
sizes?: string;
|
|
26
|
+
loading: "eager" | "lazy";
|
|
27
|
+
decoding: "async";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Builds native image props from the fixed variants Strapi generated at
|
|
31
|
+
* upload time. The selected `src` is the smallest available image at least as
|
|
32
|
+
* wide as requested; `srcSet` advertises the variants at their real widths.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getStrapiImageProps(asset: StrapiMediaAsset, options?: StrapiImageOptions): StrapiImageProps;
|
|
35
|
+
//# sourceMappingURL=image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../src/image.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACzD,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;CACvE;AAED,MAAM,WAAW,kBAAkB;IACjC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAqCD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,gBAAgB,EACvB,OAAO,GAAE,kBAAuB,GAC/B,gBAAgB,CAiClB"}
|
package/dist/image.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function resolveAssetUrl(url, mediaUrl) {
|
|
2
|
+
if (!mediaUrl && !/^https?:\/\//i.test(url)) {
|
|
3
|
+
throw new TypeError("A Strapi media URL is required for relative asset URLs");
|
|
4
|
+
}
|
|
5
|
+
let resolved;
|
|
6
|
+
try {
|
|
7
|
+
resolved = new URL(url, mediaUrl ? `${mediaUrl.replace(/\/$/, "")}/` : undefined);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new TypeError("Strapi image asset URL must be a valid HTTP or HTTPS URL");
|
|
11
|
+
}
|
|
12
|
+
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") {
|
|
13
|
+
throw new TypeError("Strapi image asset URL must use HTTP or HTTPS");
|
|
14
|
+
}
|
|
15
|
+
return resolved.toString();
|
|
16
|
+
}
|
|
17
|
+
function imageCandidates(asset, mediaUrl) {
|
|
18
|
+
const candidates = [asset, ...Object.values(asset.formats ?? {}).filter(Boolean)]
|
|
19
|
+
.filter((candidate) => !!candidate &&
|
|
20
|
+
Number.isFinite(candidate.width) &&
|
|
21
|
+
candidate.width > 0 &&
|
|
22
|
+
Number.isFinite(candidate.height) &&
|
|
23
|
+
candidate.height > 0)
|
|
24
|
+
.map((candidate) => ({
|
|
25
|
+
...candidate,
|
|
26
|
+
url: resolveAssetUrl(candidate.url, mediaUrl),
|
|
27
|
+
}))
|
|
28
|
+
.sort((left, right) => left.width - right.width);
|
|
29
|
+
return [...new Map(candidates.map((candidate) => [candidate.width, candidate])).values()];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Builds native image props from the fixed variants Strapi generated at
|
|
33
|
+
* upload time. The selected `src` is the smallest available image at least as
|
|
34
|
+
* wide as requested; `srcSet` advertises the variants at their real widths.
|
|
35
|
+
*/
|
|
36
|
+
export function getStrapiImageProps(asset, options = {}) {
|
|
37
|
+
if (!Number.isFinite(asset.width) ||
|
|
38
|
+
asset.width <= 0 ||
|
|
39
|
+
!Number.isFinite(asset.height) ||
|
|
40
|
+
asset.height <= 0) {
|
|
41
|
+
throw new TypeError("Strapi image asset dimensions must be positive finite numbers");
|
|
42
|
+
}
|
|
43
|
+
const candidates = imageCandidates(asset, options.mediaUrl);
|
|
44
|
+
if (candidates.length === 0) {
|
|
45
|
+
throw new TypeError("Strapi image asset has no usable image variants");
|
|
46
|
+
}
|
|
47
|
+
const requestedWidth = options.width ?? asset.width;
|
|
48
|
+
if (!Number.isFinite(requestedWidth) || requestedWidth <= 0) {
|
|
49
|
+
throw new TypeError("Strapi image width must be a positive finite number");
|
|
50
|
+
}
|
|
51
|
+
const selected = candidates.find((candidate) => candidate.width >= requestedWidth) ?? candidates.at(-1);
|
|
52
|
+
const renderedWidth = Math.round(Math.min(requestedWidth, selected.width));
|
|
53
|
+
const renderedHeight = Math.round((selected.height / selected.width) * renderedWidth);
|
|
54
|
+
return {
|
|
55
|
+
src: selected.url,
|
|
56
|
+
srcSet: candidates.map((candidate) => `${candidate.url} ${candidate.width}w`).join(", "),
|
|
57
|
+
alt: options.alt ?? asset.alternativeText ?? "",
|
|
58
|
+
width: renderedWidth,
|
|
59
|
+
height: renderedHeight,
|
|
60
|
+
...(options.sizes ? { sizes: options.sizes } : {}),
|
|
61
|
+
loading: options.loading ?? "lazy",
|
|
62
|
+
decoding: "async",
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type StrapiIntegrationInput } from "./config.js";
|
|
2
|
+
export declare function strapi(input?: StrapiIntegrationInput): import("@farm.js/core").DefinedIntegration<{
|
|
3
|
+
category: "cms";
|
|
4
|
+
type: string;
|
|
5
|
+
instance: import("@strapi/client").StrapiClient;
|
|
6
|
+
config: import("@farm.js/core").FarmIntegrationConfigDefinition<import("./config.js").ResolvedStrapiConfig, import("@farm.js/core").FarmSchema | undefined>;
|
|
7
|
+
log: import("@farm.js/core").FarmIntegrationLogger | undefined;
|
|
8
|
+
routes: import("@farm.js/core").FarmIntegrationRoute<unknown, unknown, import("@farm.js/core").FarmSchema | undefined>[];
|
|
9
|
+
}, import("@farm.js/core").FarmSchema | undefined>;
|
|
10
|
+
export { createStrapiClient } from "./client.js";
|
|
11
|
+
export { DEFAULT_STRAPI_WEBHOOK_PATH, DEFAULT_STRAPI_WEBHOOK_SECRET_HEADER, resolveStrapiConfig, } from "./config.js";
|
|
12
|
+
export type { ResolvedStrapiConfig, StrapiIntegrationInput, StrapiWebhookChange, StrapiWebhookOptions, } from "./config.js";
|
|
13
|
+
export { getStrapiImageProps } from "./image.js";
|
|
14
|
+
export type { StrapiImageOptions, StrapiImageProps, StrapiMediaAsset, StrapiMediaFormat, } from "./image.js";
|
|
15
|
+
export { createStrapiCollection } from "./query.js";
|
|
16
|
+
export type { StrapiDocument, StrapiQueryParams, TypedStrapiCollection } from "./query.js";
|
|
17
|
+
export { applyStrapiWebhookChange, createStrapiWebhookRoute } from "./webhook.js";
|
|
18
|
+
export type { StrapiWebhookInvalidation, StrapiWebhookRouteOptions } from "./webhook.js";
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,aAAa,CAAC;AAGrB,wBAAgB,MAAM,CAAC,KAAK,GAAE,sBAA2B;;;;;;;mDAuBxD;AAED,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EACL,2BAA2B,EAC3B,oCAAoC,EACpC,mBAAmB,GACpB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,oBAAoB,EACpB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAC3F,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAClF,YAAY,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineIntegration } from "@farm.js/core";
|
|
2
|
+
import { createStrapiClient } from "./client.js";
|
|
3
|
+
import { resolveStrapiConfig, strapiIntegrationConfig, } from "./config.js";
|
|
4
|
+
import { createStrapiWebhookRoute } from "./webhook.js";
|
|
5
|
+
export function strapi(input = {}) {
|
|
6
|
+
const config = resolveStrapiConfig(input);
|
|
7
|
+
if (!input.instance && !config.apiUrl) {
|
|
8
|
+
throw new Error("Strapi integration requires a Content API URL. Set STRAPI_API_URL, pass apiUrl to " +
|
|
9
|
+
"strapi(), or supply an existing @strapi/client instance.");
|
|
10
|
+
}
|
|
11
|
+
const routes = input.webhook && config.webhookSecret
|
|
12
|
+
? [createStrapiWebhookRoute({ ...input.webhook, secret: config.webhookSecret })]
|
|
13
|
+
: [];
|
|
14
|
+
return defineIntegration({
|
|
15
|
+
category: "cms",
|
|
16
|
+
type: "strapi",
|
|
17
|
+
instance: createStrapiClient(config, input.instance),
|
|
18
|
+
config: strapiIntegrationConfig(config, input),
|
|
19
|
+
log: input.log,
|
|
20
|
+
routes,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export { createStrapiClient } from "./client.js";
|
|
24
|
+
export { DEFAULT_STRAPI_WEBHOOK_PATH, DEFAULT_STRAPI_WEBHOOK_SECRET_HEADER, resolveStrapiConfig, } from "./config.js";
|
|
25
|
+
export { getStrapiImageProps } from "./image.js";
|
|
26
|
+
export { createStrapiCollection } from "./query.js";
|
|
27
|
+
export { applyStrapiWebhookChange, createStrapiWebhookRoute } from "./webhook.js";
|
package/dist/query.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { API, StrapiClient } from "@strapi/client";
|
|
2
|
+
export type StrapiQueryParams = API.BaseQueryParams;
|
|
3
|
+
export type StrapiDocument = API.Document;
|
|
4
|
+
export interface TypedStrapiCollection<TDocument extends StrapiDocument> {
|
|
5
|
+
find(query?: StrapiQueryParams): Promise<TDocument[]>;
|
|
6
|
+
findOne(documentId: string, query?: StrapiQueryParams): Promise<TDocument>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Adds an application-owned document type to the official Strapi manager and
|
|
10
|
+
* unwraps the REST `data` envelope. Use the returned methods inside
|
|
11
|
+
* `createServerQuery` so provider credentials remain on the server.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createStrapiCollection<TDocument extends StrapiDocument = StrapiDocument>(client: StrapiClient, resource: string): TypedStrapiCollection<TDocument>;
|
|
14
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAExD,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,eAAe,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,QAAQ,CAAC;AAE1C,MAAM,WAAW,qBAAqB,CAAC,SAAS,SAAS,cAAc;IACrE,IAAI,CAAC,KAAK,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACtD,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;CAC5E;AA4BD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,SAAS,cAAc,GAAG,cAAc,EACtF,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,MAAM,GACf,qBAAqB,CAAC,SAAS,CAAC,CAsBlC"}
|
package/dist/query.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function assertResourceName(resource) {
|
|
2
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(resource)) {
|
|
3
|
+
throw new TypeError(`Strapi resource ${JSON.stringify(resource)} must be a collection API id, not a path`);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
function assertDocumentId(documentId) {
|
|
7
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(documentId)) {
|
|
8
|
+
throw new TypeError(`Strapi document id ${JSON.stringify(documentId)} must be an id, not a path`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function assertDocument(value, operation) {
|
|
12
|
+
if (!value ||
|
|
13
|
+
typeof value !== "object" ||
|
|
14
|
+
typeof value.documentId !== "string") {
|
|
15
|
+
throw new TypeError(`${operation} received a response without a Strapi document`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Adds an application-owned document type to the official Strapi manager and
|
|
20
|
+
* unwraps the REST `data` envelope. Use the returned methods inside
|
|
21
|
+
* `createServerQuery` so provider credentials remain on the server.
|
|
22
|
+
*/
|
|
23
|
+
export function createStrapiCollection(client, resource) {
|
|
24
|
+
assertResourceName(resource);
|
|
25
|
+
const manager = client.collection(resource);
|
|
26
|
+
return {
|
|
27
|
+
async find(query) {
|
|
28
|
+
const response = await manager.find(query);
|
|
29
|
+
if (!response || !Array.isArray(response.data)) {
|
|
30
|
+
throw new TypeError(`Strapi collection ${JSON.stringify(resource)} returned no data array`);
|
|
31
|
+
}
|
|
32
|
+
for (const document of response.data) {
|
|
33
|
+
assertDocument(document, `Strapi collection ${JSON.stringify(resource)}`);
|
|
34
|
+
}
|
|
35
|
+
return response.data;
|
|
36
|
+
},
|
|
37
|
+
async findOne(documentId, query) {
|
|
38
|
+
assertDocumentId(documentId);
|
|
39
|
+
const response = await manager.findOne(documentId, query);
|
|
40
|
+
assertDocument(response?.data, `Strapi collection ${JSON.stringify(resource)}`);
|
|
41
|
+
return response.data;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type FarmIntegrationRoute, type RouteDataCacheKey } from "@farm.js/core";
|
|
2
|
+
import { type StrapiWebhookChange, type StrapiWebhookOptions } from "./config.js";
|
|
3
|
+
export interface StrapiWebhookInvalidation {
|
|
4
|
+
invalidate(key: RouteDataCacheKey): void | Promise<void>;
|
|
5
|
+
revalidatePath(path: string): void | Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface StrapiWebhookRouteOptions extends Omit<StrapiWebhookOptions, "secret"> {
|
|
8
|
+
secret: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function applyStrapiWebhookChange(change: StrapiWebhookChange | void, invalidation?: StrapiWebhookInvalidation): Promise<number>;
|
|
11
|
+
export declare function createStrapiWebhookRoute(options: StrapiWebhookRouteOptions, invalidation?: StrapiWebhookInvalidation): FarmIntegrationRoute;
|
|
12
|
+
//# sourceMappingURL=webhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EAC1B,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,yBAAyB;IACxC,UAAU,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD;AAID,MAAM,WAAW,yBAA0B,SAAQ,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAC;IACrF,MAAM,EAAE,MAAM,CAAC;CAChB;AAsBD,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,mBAAmB,GAAG,IAAI,EAClC,YAAY,GAAE,yBAA4C,GACzD,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,yBAAyB,EAClC,YAAY,GAAE,yBAA4C,GACzD,oBAAoB,CAyCtB"}
|
package/dist/webhook.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { integrationRoute, invalidate, revalidatePath, } from "@farm.js/core";
|
|
2
|
+
import { DEFAULT_STRAPI_WEBHOOK_PATH, DEFAULT_STRAPI_WEBHOOK_SECRET_HEADER, } from "./config.js";
|
|
3
|
+
const farmInvalidation = { invalidate, revalidatePath };
|
|
4
|
+
function json(status, body) {
|
|
5
|
+
return new Response(JSON.stringify(body), {
|
|
6
|
+
status,
|
|
7
|
+
headers: { "content-type": "application/json" },
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
async function secretsEqual(expected, received) {
|
|
11
|
+
const encoded = new TextEncoder();
|
|
12
|
+
const [expectedDigest, receivedDigest] = await Promise.all([
|
|
13
|
+
crypto.subtle.digest("SHA-256", encoded.encode(expected)),
|
|
14
|
+
crypto.subtle.digest("SHA-256", encoded.encode(received)),
|
|
15
|
+
]);
|
|
16
|
+
const left = new Uint8Array(expectedDigest);
|
|
17
|
+
const right = new Uint8Array(receivedDigest);
|
|
18
|
+
let difference = 0;
|
|
19
|
+
for (let index = 0; index < left.length; index += 1)
|
|
20
|
+
difference |= left[index] ^ right[index];
|
|
21
|
+
return difference === 0;
|
|
22
|
+
}
|
|
23
|
+
export async function applyStrapiWebhookChange(change, invalidation = farmInvalidation) {
|
|
24
|
+
if (!change)
|
|
25
|
+
return 0;
|
|
26
|
+
const keys = [...new Map((change.keys ?? []).map((key) => [JSON.stringify(key), key])).values()];
|
|
27
|
+
const paths = [...new Set(change.paths ?? [])];
|
|
28
|
+
await Promise.all([
|
|
29
|
+
...keys.map((key) => invalidation.invalidate(key)),
|
|
30
|
+
...paths.map((path) => invalidation.revalidatePath(path)),
|
|
31
|
+
]);
|
|
32
|
+
return keys.length + paths.length;
|
|
33
|
+
}
|
|
34
|
+
export function createStrapiWebhookRoute(options, invalidation = farmInvalidation) {
|
|
35
|
+
if (!options.secret)
|
|
36
|
+
throw new TypeError("Strapi webhook requires a non-empty secret");
|
|
37
|
+
const secretHeader = options.secretHeader ?? DEFAULT_STRAPI_WEBHOOK_SECRET_HEADER;
|
|
38
|
+
try {
|
|
39
|
+
new Headers({ [secretHeader]: "validate" });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new TypeError("Strapi webhook secretHeader must be a valid HTTP header name");
|
|
43
|
+
}
|
|
44
|
+
return integrationRoute.post(options.path ?? DEFAULT_STRAPI_WEBHOOK_PATH, {
|
|
45
|
+
// Authentication happens before the integration runtime performs its
|
|
46
|
+
// ordinary JSON validation. The handler parses the authenticated body.
|
|
47
|
+
bodyFormat: "none",
|
|
48
|
+
async handler(request) {
|
|
49
|
+
const presentedSecret = request.headers.get(secretHeader);
|
|
50
|
+
if (!presentedSecret || !(await secretsEqual(options.secret, presentedSecret))) {
|
|
51
|
+
return json(401, { error: "Invalid webhook secret" });
|
|
52
|
+
}
|
|
53
|
+
let payload;
|
|
54
|
+
try {
|
|
55
|
+
const value = await request.json();
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
57
|
+
return json(400, { error: "Body must be a JSON object" });
|
|
58
|
+
}
|
|
59
|
+
payload = value;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return json(400, { error: "Body is not JSON" });
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const change = await options.onChange(payload);
|
|
66
|
+
const targets = await applyStrapiWebhookChange(change, invalidation);
|
|
67
|
+
return json(200, { targets });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// A mapping or cache failure should be visible to the webhook sender,
|
|
71
|
+
// without reflecting application errors or credentials in the body.
|
|
72
|
+
return json(500, { error: "Failed to apply change" });
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@farm.js/strapi",
|
|
3
|
+
"version": "0.1.1-beta.4",
|
|
4
|
+
"description": "Strapi CMS integration for Farm.js",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/farming-labs/farm.js",
|
|
9
|
+
"directory": "packages/farm-strapi"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@farm.js/core": "0.1.1-beta.4",
|
|
29
|
+
"@farm.js/integration-utils": "0.1.1-beta.4"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@strapi/client": "^1.6.2",
|
|
33
|
+
"typescript": "^5.3.3",
|
|
34
|
+
"vitest": "^3.2.7"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@strapi/client": "^1.6.2"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|
|
41
|
+
"dev": "tsc --watch",
|
|
42
|
+
"type-check": "tsc --noEmit -p tsconfig.test.json",
|
|
43
|
+
"test": "vitest run"
|
|
44
|
+
}
|
|
45
|
+
}
|