@flexacms/next 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/dist/index.cjs +120 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan Mercedes
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flexacms/next
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Flexa](https://github.com/flexacms) for the Next.js App Router: server-side fetching through Next's data cache, tagged per entity and entry for precise revalidation.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @flexacms/next
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```env
|
|
10
|
+
FLEXA_URL=https://cms.example.com
|
|
11
|
+
FLEXA_API_KEY=flx_...
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// lib/flexa.ts
|
|
16
|
+
import { createFlexaClient } from '@flexacms/next';
|
|
17
|
+
import type { FlexaSchema } from './flexa'; // optional: npx @flexacms/cli generate
|
|
18
|
+
|
|
19
|
+
export const flexa = createFlexaClient<FlexaSchema>({ revalidate: 300 });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
// app/blog/[slug]/page.tsx (Server Component)
|
|
24
|
+
import { notFound } from 'next/navigation';
|
|
25
|
+
import { flexa } from '@/lib/flexa';
|
|
26
|
+
|
|
27
|
+
export default async function Post({
|
|
28
|
+
params,
|
|
29
|
+
}: {
|
|
30
|
+
params: Promise<{ slug: string }>;
|
|
31
|
+
}) {
|
|
32
|
+
const post = await flexa.entity('posts').find((await params).slug);
|
|
33
|
+
|
|
34
|
+
if (!post) notFound();
|
|
35
|
+
|
|
36
|
+
return <h1>{post.title}</h1>;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Reads are cached and tagged `flexa`, `flexa:<entity>` and `flexa:<entity>:<id or slug>`; writes are never cached. `revalidate` sets how long cached responses live (default 60 seconds, `false` to keep them until revalidated).
|
|
41
|
+
|
|
42
|
+
## Revalidation
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { revalidateFlexa } from '@flexacms/next';
|
|
46
|
+
|
|
47
|
+
revalidateFlexa(); // everything
|
|
48
|
+
revalidateFlexa('posts'); // one entity
|
|
49
|
+
revalidateFlexa('posts', 'hello'); // one entry
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or expose a route for webhooks and deploy hooks:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// app/api/flexa/revalidate/route.ts
|
|
56
|
+
import { createRevalidateHandler } from '@flexacms/next';
|
|
57
|
+
|
|
58
|
+
export const POST = createRevalidateHandler({
|
|
59
|
+
secret: process.env.FLEXA_REVALIDATE_SECRET,
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl -X POST https://site.example.com/api/flexa/revalidate \
|
|
65
|
+
-H "Authorization: Bearer $FLEXA_REVALIDATE_SECRET" \
|
|
66
|
+
-d '{"entity":"posts","entry":"hello"}'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> Import `createFlexaClient` only from server code: the API key must not reach the browser. For client components, use [`@flexacms/react`](../react) with a Read-only key.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
FLEXA_TAG: () => FLEXA_TAG,
|
|
25
|
+
createFlexaClient: () => createFlexaClient,
|
|
26
|
+
createRevalidateHandler: () => createRevalidateHandler,
|
|
27
|
+
flexaTags: () => flexaTags,
|
|
28
|
+
revalidateFlexa: () => revalidateFlexa
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
var import_client = require("@flexacms/client");
|
|
32
|
+
var import_cache = require("next/cache");
|
|
33
|
+
__reExport(index_exports, require("@flexacms/client"), module.exports);
|
|
34
|
+
var FLEXA_TAG = "flexa";
|
|
35
|
+
function requestUrl(input) {
|
|
36
|
+
if (typeof input === "string") {
|
|
37
|
+
return input;
|
|
38
|
+
}
|
|
39
|
+
return input instanceof URL ? input.href : input.url;
|
|
40
|
+
}
|
|
41
|
+
function flexaTags(path) {
|
|
42
|
+
const [entity, entry] = path.split("?")[0].split("/").filter(Boolean).map(decodeURIComponent);
|
|
43
|
+
const tags = [FLEXA_TAG];
|
|
44
|
+
if (entity) {
|
|
45
|
+
tags.push(`${FLEXA_TAG}:${entity}`);
|
|
46
|
+
}
|
|
47
|
+
if (entity && entry && entry !== "schema") {
|
|
48
|
+
tags.push(`${FLEXA_TAG}:${entity}:${entry}`);
|
|
49
|
+
}
|
|
50
|
+
return tags;
|
|
51
|
+
}
|
|
52
|
+
function createFlexaClient(options = {}) {
|
|
53
|
+
const { revalidate = 60, ...clientOptions } = options;
|
|
54
|
+
const url = clientOptions.url ?? process.env.FLEXA_URL;
|
|
55
|
+
const apiKey = clientOptions.apiKey ?? process.env.FLEXA_API_KEY;
|
|
56
|
+
if (!url || !apiKey) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'Flexa: set the FLEXA_URL and FLEXA_API_KEY environment variables, or pass "url" and "apiKey".'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const baseFetch = clientOptions.fetch ?? globalThis.fetch;
|
|
62
|
+
let client;
|
|
63
|
+
const cachedFetch = ((input, init = {}) => {
|
|
64
|
+
if ((init.method ?? "GET").toUpperCase() !== "GET") {
|
|
65
|
+
return baseFetch(input, { ...init, cache: "no-store" });
|
|
66
|
+
}
|
|
67
|
+
const path = new URL(requestUrl(input)).pathname.slice(
|
|
68
|
+
new URL(client.baseUrl).pathname.length
|
|
69
|
+
);
|
|
70
|
+
return baseFetch(input, {
|
|
71
|
+
...init,
|
|
72
|
+
next: {
|
|
73
|
+
revalidate,
|
|
74
|
+
...init.next,
|
|
75
|
+
tags: [...flexaTags(path), ...init.next?.tags ?? []]
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
client = (0, import_client.createClient)({
|
|
80
|
+
...clientOptions,
|
|
81
|
+
url,
|
|
82
|
+
apiKey,
|
|
83
|
+
fetch: cachedFetch
|
|
84
|
+
});
|
|
85
|
+
return client;
|
|
86
|
+
}
|
|
87
|
+
function revalidateFlexa(entity, idOrSlug) {
|
|
88
|
+
const tag = entity === void 0 ? FLEXA_TAG : idOrSlug === void 0 ? `${FLEXA_TAG}:${entity}` : `${FLEXA_TAG}:${entity}:${idOrSlug}`;
|
|
89
|
+
(0, import_cache.revalidateTag)(tag, "max");
|
|
90
|
+
}
|
|
91
|
+
function createRevalidateHandler(options) {
|
|
92
|
+
return async function POST(request) {
|
|
93
|
+
if (!options.secret || request.headers.get("authorization") !== `Bearer ${options.secret}`) {
|
|
94
|
+
return Response.json({ message: "Unauthorized." }, { status: 401 });
|
|
95
|
+
}
|
|
96
|
+
let body = {};
|
|
97
|
+
try {
|
|
98
|
+
body = await request.json();
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
const entity = typeof body.entity === "string" && body.entity !== "" ? body.entity : void 0;
|
|
102
|
+
const entry = typeof body.entry === "string" || typeof body.entry === "number" ? body.entry : void 0;
|
|
103
|
+
revalidateFlexa(entity, entity === void 0 ? void 0 : entry);
|
|
104
|
+
return Response.json({
|
|
105
|
+
revalidated: true,
|
|
106
|
+
entity: entity ?? null,
|
|
107
|
+
entry: entity === void 0 ? null : entry ?? null
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
112
|
+
0 && (module.exports = {
|
|
113
|
+
FLEXA_TAG,
|
|
114
|
+
createFlexaClient,
|
|
115
|
+
createRevalidateHandler,
|
|
116
|
+
flexaTags,
|
|
117
|
+
revalidateFlexa,
|
|
118
|
+
...require("@flexacms/client")
|
|
119
|
+
});
|
|
120
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { createClient } from '@flexacms/client';\nimport type {\n ContentSchema,\n FlexaClient,\n FlexaClientOptions,\n} from '@flexacms/client';\nimport { revalidateTag } from 'next/cache';\n\nexport * from '@flexacms/client';\n\n/** Tag on every cached Flexa response. */\nexport const FLEXA_TAG = 'flexa';\n\nexport interface FlexaNextOptions extends Partial<FlexaClientOptions> {\n /**\n * Seconds before a cached response is refreshed in the background, or\n * `false` to keep it until it is revalidated by tag. Defaults to 60.\n */\n revalidate?: number | false;\n}\n\ntype NextFetchInit = RequestInit & {\n next?: { revalidate?: number | false; tags?: string[] };\n};\n\n/** The URL of any `fetch` input (string, URL or Request). */\nfunction requestUrl(input: string | URL | Request): string {\n if (typeof input === 'string') {\n return input;\n }\n\n return input instanceof URL ? input.href : input.url;\n}\n\n/**\n * Cache tags for an API path: `flexa`, `flexa:<entity>` and, for one entry,\n * `flexa:<entity>:<id or slug>`.\n */\nexport function flexaTags(path: string): string[] {\n const [entity, entry] = path\n .split('?')[0]!\n .split('/')\n .filter(Boolean)\n .map(decodeURIComponent);\n const tags = [FLEXA_TAG];\n\n if (entity) {\n tags.push(`${FLEXA_TAG}:${entity}`);\n }\n\n if (entity && entry && entry !== 'schema') {\n tags.push(`${FLEXA_TAG}:${entity}:${entry}`);\n }\n\n return tags;\n}\n\n/**\n * A Flexa client for Server Components, Route Handlers and Server Actions.\n *\n * Reads go through Next's data cache, tagged per entity and entry so they can\n * be refreshed with `revalidateFlexa()`; writes are never cached. The URL and\n * API key default to the `FLEXA_URL` and `FLEXA_API_KEY` environment variables.\n * Only use it on the server: the API key must not reach the browser.\n */\nexport function createFlexaClient<TSchema extends object = ContentSchema>(\n options: FlexaNextOptions = {},\n): FlexaClient<TSchema> {\n const { revalidate = 60, ...clientOptions } = options;\n const url = clientOptions.url ?? process.env.FLEXA_URL;\n const apiKey = clientOptions.apiKey ?? process.env.FLEXA_API_KEY;\n\n if (!url || !apiKey) {\n throw new Error(\n 'Flexa: set the FLEXA_URL and FLEXA_API_KEY environment variables, or pass \"url\" and \"apiKey\".',\n );\n }\n\n const baseFetch = clientOptions.fetch ?? globalThis.fetch;\n let client: FlexaClient<TSchema>;\n\n const cachedFetch = ((\n input: string | URL | Request,\n init: NextFetchInit = {},\n ) => {\n if ((init.method ?? 'GET').toUpperCase() !== 'GET') {\n return baseFetch(input, { ...init, cache: 'no-store' });\n }\n\n const path = new URL(requestUrl(input)).pathname.slice(\n new URL(client.baseUrl).pathname.length,\n );\n\n return baseFetch(input, {\n ...init,\n next: {\n revalidate,\n ...init.next,\n tags: [...flexaTags(path), ...(init.next?.tags ?? [])],\n },\n } as RequestInit);\n }) as typeof fetch;\n\n client = createClient<TSchema>({\n ...clientOptions,\n url,\n apiKey,\n fetch: cachedFetch,\n });\n\n return client;\n}\n\n/**\n * Refresh cached Flexa content: everything, one entity, or one entry. Call it\n * from a Server Action or Route Handler after content changes.\n */\nexport function revalidateFlexa(\n entity?: string,\n idOrSlug?: number | string,\n): void {\n const tag =\n entity === undefined\n ? FLEXA_TAG\n : idOrSlug === undefined\n ? `${FLEXA_TAG}:${entity}`\n : `${FLEXA_TAG}:${entity}:${idOrSlug}`;\n\n revalidateTag(tag, 'max');\n}\n\n/**\n * A `POST` Route Handler that revalidates Flexa content, for webhooks or\n * deploy hooks. Send `{ \"entity\": \"posts\", \"entry\": \"hello-world\" }` (both\n * optional) with the header `Authorization: Bearer <secret>`.\n *\n * // app/api/flexa/revalidate/route.ts\n * export const POST = createRevalidateHandler({ secret: process.env.FLEXA_REVALIDATE_SECRET });\n */\nexport function createRevalidateHandler(options: {\n secret: string | undefined;\n}) {\n return async function POST(request: Request): Promise<Response> {\n if (\n !options.secret ||\n request.headers.get('authorization') !== `Bearer ${options.secret}`\n ) {\n return Response.json({ message: 'Unauthorized.' }, { status: 401 });\n }\n\n let body: { entity?: unknown; entry?: unknown } = {};\n\n try {\n body = (await request.json()) as typeof body;\n } catch {\n // An empty body revalidates everything.\n }\n\n const entity =\n typeof body.entity === 'string' && body.entity !== ''\n ? body.entity\n : undefined;\n const entry =\n typeof body.entry === 'string' || typeof body.entry === 'number'\n ? body.entry\n : undefined;\n\n revalidateFlexa(entity, entity === undefined ? undefined : entry);\n\n return Response.json({\n revalidated: true,\n entity: entity ?? null,\n entry: entity === undefined ? null : (entry ?? null),\n });\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { FlexaClientOptions, ContentSchema, FlexaClient } from '@flexacms/client';
|
|
2
|
+
export * from '@flexacms/client';
|
|
3
|
+
|
|
4
|
+
/** Tag on every cached Flexa response. */
|
|
5
|
+
declare const FLEXA_TAG = "flexa";
|
|
6
|
+
interface FlexaNextOptions extends Partial<FlexaClientOptions> {
|
|
7
|
+
/**
|
|
8
|
+
* Seconds before a cached response is refreshed in the background, or
|
|
9
|
+
* `false` to keep it until it is revalidated by tag. Defaults to 60.
|
|
10
|
+
*/
|
|
11
|
+
revalidate?: number | false;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Cache tags for an API path: `flexa`, `flexa:<entity>` and, for one entry,
|
|
15
|
+
* `flexa:<entity>:<id or slug>`.
|
|
16
|
+
*/
|
|
17
|
+
declare function flexaTags(path: string): string[];
|
|
18
|
+
/**
|
|
19
|
+
* A Flexa client for Server Components, Route Handlers and Server Actions.
|
|
20
|
+
*
|
|
21
|
+
* Reads go through Next's data cache, tagged per entity and entry so they can
|
|
22
|
+
* be refreshed with `revalidateFlexa()`; writes are never cached. The URL and
|
|
23
|
+
* API key default to the `FLEXA_URL` and `FLEXA_API_KEY` environment variables.
|
|
24
|
+
* Only use it on the server: the API key must not reach the browser.
|
|
25
|
+
*/
|
|
26
|
+
declare function createFlexaClient<TSchema extends object = ContentSchema>(options?: FlexaNextOptions): FlexaClient<TSchema>;
|
|
27
|
+
/**
|
|
28
|
+
* Refresh cached Flexa content: everything, one entity, or one entry. Call it
|
|
29
|
+
* from a Server Action or Route Handler after content changes.
|
|
30
|
+
*/
|
|
31
|
+
declare function revalidateFlexa(entity?: string, idOrSlug?: number | string): void;
|
|
32
|
+
/**
|
|
33
|
+
* A `POST` Route Handler that revalidates Flexa content, for webhooks or
|
|
34
|
+
* deploy hooks. Send `{ "entity": "posts", "entry": "hello-world" }` (both
|
|
35
|
+
* optional) with the header `Authorization: Bearer <secret>`.
|
|
36
|
+
*
|
|
37
|
+
* // app/api/flexa/revalidate/route.ts
|
|
38
|
+
* export const POST = createRevalidateHandler({ secret: process.env.FLEXA_REVALIDATE_SECRET });
|
|
39
|
+
*/
|
|
40
|
+
declare function createRevalidateHandler(options: {
|
|
41
|
+
secret: string | undefined;
|
|
42
|
+
}): (request: Request) => Promise<Response>;
|
|
43
|
+
|
|
44
|
+
export { FLEXA_TAG, type FlexaNextOptions, createFlexaClient, createRevalidateHandler, flexaTags, revalidateFlexa };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { FlexaClientOptions, ContentSchema, FlexaClient } from '@flexacms/client';
|
|
2
|
+
export * from '@flexacms/client';
|
|
3
|
+
|
|
4
|
+
/** Tag on every cached Flexa response. */
|
|
5
|
+
declare const FLEXA_TAG = "flexa";
|
|
6
|
+
interface FlexaNextOptions extends Partial<FlexaClientOptions> {
|
|
7
|
+
/**
|
|
8
|
+
* Seconds before a cached response is refreshed in the background, or
|
|
9
|
+
* `false` to keep it until it is revalidated by tag. Defaults to 60.
|
|
10
|
+
*/
|
|
11
|
+
revalidate?: number | false;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Cache tags for an API path: `flexa`, `flexa:<entity>` and, for one entry,
|
|
15
|
+
* `flexa:<entity>:<id or slug>`.
|
|
16
|
+
*/
|
|
17
|
+
declare function flexaTags(path: string): string[];
|
|
18
|
+
/**
|
|
19
|
+
* A Flexa client for Server Components, Route Handlers and Server Actions.
|
|
20
|
+
*
|
|
21
|
+
* Reads go through Next's data cache, tagged per entity and entry so they can
|
|
22
|
+
* be refreshed with `revalidateFlexa()`; writes are never cached. The URL and
|
|
23
|
+
* API key default to the `FLEXA_URL` and `FLEXA_API_KEY` environment variables.
|
|
24
|
+
* Only use it on the server: the API key must not reach the browser.
|
|
25
|
+
*/
|
|
26
|
+
declare function createFlexaClient<TSchema extends object = ContentSchema>(options?: FlexaNextOptions): FlexaClient<TSchema>;
|
|
27
|
+
/**
|
|
28
|
+
* Refresh cached Flexa content: everything, one entity, or one entry. Call it
|
|
29
|
+
* from a Server Action or Route Handler after content changes.
|
|
30
|
+
*/
|
|
31
|
+
declare function revalidateFlexa(entity?: string, idOrSlug?: number | string): void;
|
|
32
|
+
/**
|
|
33
|
+
* A `POST` Route Handler that revalidates Flexa content, for webhooks or
|
|
34
|
+
* deploy hooks. Send `{ "entity": "posts", "entry": "hello-world" }` (both
|
|
35
|
+
* optional) with the header `Authorization: Bearer <secret>`.
|
|
36
|
+
*
|
|
37
|
+
* // app/api/flexa/revalidate/route.ts
|
|
38
|
+
* export const POST = createRevalidateHandler({ secret: process.env.FLEXA_REVALIDATE_SECRET });
|
|
39
|
+
*/
|
|
40
|
+
declare function createRevalidateHandler(options: {
|
|
41
|
+
secret: string | undefined;
|
|
42
|
+
}): (request: Request) => Promise<Response>;
|
|
43
|
+
|
|
44
|
+
export { FLEXA_TAG, type FlexaNextOptions, createFlexaClient, createRevalidateHandler, flexaTags, revalidateFlexa };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createClient } from "@flexacms/client";
|
|
3
|
+
import { revalidateTag } from "next/cache";
|
|
4
|
+
export * from "@flexacms/client";
|
|
5
|
+
var FLEXA_TAG = "flexa";
|
|
6
|
+
function requestUrl(input) {
|
|
7
|
+
if (typeof input === "string") {
|
|
8
|
+
return input;
|
|
9
|
+
}
|
|
10
|
+
return input instanceof URL ? input.href : input.url;
|
|
11
|
+
}
|
|
12
|
+
function flexaTags(path) {
|
|
13
|
+
const [entity, entry] = path.split("?")[0].split("/").filter(Boolean).map(decodeURIComponent);
|
|
14
|
+
const tags = [FLEXA_TAG];
|
|
15
|
+
if (entity) {
|
|
16
|
+
tags.push(`${FLEXA_TAG}:${entity}`);
|
|
17
|
+
}
|
|
18
|
+
if (entity && entry && entry !== "schema") {
|
|
19
|
+
tags.push(`${FLEXA_TAG}:${entity}:${entry}`);
|
|
20
|
+
}
|
|
21
|
+
return tags;
|
|
22
|
+
}
|
|
23
|
+
function createFlexaClient(options = {}) {
|
|
24
|
+
const { revalidate = 60, ...clientOptions } = options;
|
|
25
|
+
const url = clientOptions.url ?? process.env.FLEXA_URL;
|
|
26
|
+
const apiKey = clientOptions.apiKey ?? process.env.FLEXA_API_KEY;
|
|
27
|
+
if (!url || !apiKey) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'Flexa: set the FLEXA_URL and FLEXA_API_KEY environment variables, or pass "url" and "apiKey".'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const baseFetch = clientOptions.fetch ?? globalThis.fetch;
|
|
33
|
+
let client;
|
|
34
|
+
const cachedFetch = ((input, init = {}) => {
|
|
35
|
+
if ((init.method ?? "GET").toUpperCase() !== "GET") {
|
|
36
|
+
return baseFetch(input, { ...init, cache: "no-store" });
|
|
37
|
+
}
|
|
38
|
+
const path = new URL(requestUrl(input)).pathname.slice(
|
|
39
|
+
new URL(client.baseUrl).pathname.length
|
|
40
|
+
);
|
|
41
|
+
return baseFetch(input, {
|
|
42
|
+
...init,
|
|
43
|
+
next: {
|
|
44
|
+
revalidate,
|
|
45
|
+
...init.next,
|
|
46
|
+
tags: [...flexaTags(path), ...init.next?.tags ?? []]
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
client = createClient({
|
|
51
|
+
...clientOptions,
|
|
52
|
+
url,
|
|
53
|
+
apiKey,
|
|
54
|
+
fetch: cachedFetch
|
|
55
|
+
});
|
|
56
|
+
return client;
|
|
57
|
+
}
|
|
58
|
+
function revalidateFlexa(entity, idOrSlug) {
|
|
59
|
+
const tag = entity === void 0 ? FLEXA_TAG : idOrSlug === void 0 ? `${FLEXA_TAG}:${entity}` : `${FLEXA_TAG}:${entity}:${idOrSlug}`;
|
|
60
|
+
revalidateTag(tag, "max");
|
|
61
|
+
}
|
|
62
|
+
function createRevalidateHandler(options) {
|
|
63
|
+
return async function POST(request) {
|
|
64
|
+
if (!options.secret || request.headers.get("authorization") !== `Bearer ${options.secret}`) {
|
|
65
|
+
return Response.json({ message: "Unauthorized." }, { status: 401 });
|
|
66
|
+
}
|
|
67
|
+
let body = {};
|
|
68
|
+
try {
|
|
69
|
+
body = await request.json();
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
const entity = typeof body.entity === "string" && body.entity !== "" ? body.entity : void 0;
|
|
73
|
+
const entry = typeof body.entry === "string" || typeof body.entry === "number" ? body.entry : void 0;
|
|
74
|
+
revalidateFlexa(entity, entity === void 0 ? void 0 : entry);
|
|
75
|
+
return Response.json({
|
|
76
|
+
revalidated: true,
|
|
77
|
+
entity: entity ?? null,
|
|
78
|
+
entry: entity === void 0 ? null : entry ?? null
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
FLEXA_TAG,
|
|
84
|
+
createFlexaClient,
|
|
85
|
+
createRevalidateHandler,
|
|
86
|
+
flexaTags,
|
|
87
|
+
revalidateFlexa
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { createClient } from '@flexacms/client';\nimport type {\n ContentSchema,\n FlexaClient,\n FlexaClientOptions,\n} from '@flexacms/client';\nimport { revalidateTag } from 'next/cache';\n\nexport * from '@flexacms/client';\n\n/** Tag on every cached Flexa response. */\nexport const FLEXA_TAG = 'flexa';\n\nexport interface FlexaNextOptions extends Partial<FlexaClientOptions> {\n /**\n * Seconds before a cached response is refreshed in the background, or\n * `false` to keep it until it is revalidated by tag. Defaults to 60.\n */\n revalidate?: number | false;\n}\n\ntype NextFetchInit = RequestInit & {\n next?: { revalidate?: number | false; tags?: string[] };\n};\n\n/** The URL of any `fetch` input (string, URL or Request). */\nfunction requestUrl(input: string | URL | Request): string {\n if (typeof input === 'string') {\n return input;\n }\n\n return input instanceof URL ? input.href : input.url;\n}\n\n/**\n * Cache tags for an API path: `flexa`, `flexa:<entity>` and, for one entry,\n * `flexa:<entity>:<id or slug>`.\n */\nexport function flexaTags(path: string): string[] {\n const [entity, entry] = path\n .split('?')[0]!\n .split('/')\n .filter(Boolean)\n .map(decodeURIComponent);\n const tags = [FLEXA_TAG];\n\n if (entity) {\n tags.push(`${FLEXA_TAG}:${entity}`);\n }\n\n if (entity && entry && entry !== 'schema') {\n tags.push(`${FLEXA_TAG}:${entity}:${entry}`);\n }\n\n return tags;\n}\n\n/**\n * A Flexa client for Server Components, Route Handlers and Server Actions.\n *\n * Reads go through Next's data cache, tagged per entity and entry so they can\n * be refreshed with `revalidateFlexa()`; writes are never cached. The URL and\n * API key default to the `FLEXA_URL` and `FLEXA_API_KEY` environment variables.\n * Only use it on the server: the API key must not reach the browser.\n */\nexport function createFlexaClient<TSchema extends object = ContentSchema>(\n options: FlexaNextOptions = {},\n): FlexaClient<TSchema> {\n const { revalidate = 60, ...clientOptions } = options;\n const url = clientOptions.url ?? process.env.FLEXA_URL;\n const apiKey = clientOptions.apiKey ?? process.env.FLEXA_API_KEY;\n\n if (!url || !apiKey) {\n throw new Error(\n 'Flexa: set the FLEXA_URL and FLEXA_API_KEY environment variables, or pass \"url\" and \"apiKey\".',\n );\n }\n\n const baseFetch = clientOptions.fetch ?? globalThis.fetch;\n let client: FlexaClient<TSchema>;\n\n const cachedFetch = ((\n input: string | URL | Request,\n init: NextFetchInit = {},\n ) => {\n if ((init.method ?? 'GET').toUpperCase() !== 'GET') {\n return baseFetch(input, { ...init, cache: 'no-store' });\n }\n\n const path = new URL(requestUrl(input)).pathname.slice(\n new URL(client.baseUrl).pathname.length,\n );\n\n return baseFetch(input, {\n ...init,\n next: {\n revalidate,\n ...init.next,\n tags: [...flexaTags(path), ...(init.next?.tags ?? [])],\n },\n } as RequestInit);\n }) as typeof fetch;\n\n client = createClient<TSchema>({\n ...clientOptions,\n url,\n apiKey,\n fetch: cachedFetch,\n });\n\n return client;\n}\n\n/**\n * Refresh cached Flexa content: everything, one entity, or one entry. Call it\n * from a Server Action or Route Handler after content changes.\n */\nexport function revalidateFlexa(\n entity?: string,\n idOrSlug?: number | string,\n): void {\n const tag =\n entity === undefined\n ? FLEXA_TAG\n : idOrSlug === undefined\n ? `${FLEXA_TAG}:${entity}`\n : `${FLEXA_TAG}:${entity}:${idOrSlug}`;\n\n revalidateTag(tag, 'max');\n}\n\n/**\n * A `POST` Route Handler that revalidates Flexa content, for webhooks or\n * deploy hooks. Send `{ \"entity\": \"posts\", \"entry\": \"hello-world\" }` (both\n * optional) with the header `Authorization: Bearer <secret>`.\n *\n * // app/api/flexa/revalidate/route.ts\n * export const POST = createRevalidateHandler({ secret: process.env.FLEXA_REVALIDATE_SECRET });\n */\nexport function createRevalidateHandler(options: {\n secret: string | undefined;\n}) {\n return async function POST(request: Request): Promise<Response> {\n if (\n !options.secret ||\n request.headers.get('authorization') !== `Bearer ${options.secret}`\n ) {\n return Response.json({ message: 'Unauthorized.' }, { status: 401 });\n }\n\n let body: { entity?: unknown; entry?: unknown } = {};\n\n try {\n body = (await request.json()) as typeof body;\n } catch {\n // An empty body revalidates everything.\n }\n\n const entity =\n typeof body.entity === 'string' && body.entity !== ''\n ? body.entity\n : undefined;\n const entry =\n typeof body.entry === 'string' || typeof body.entry === 'number'\n ? body.entry\n : undefined;\n\n revalidateFlexa(entity, entity === undefined ? undefined : entry);\n\n return Response.json({\n revalidated: true,\n entity: entity ?? null,\n entry: entity === undefined ? null : (entry ?? null),\n });\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@flexacms/next",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Next.js integration for the Flexa headless CMS: cached server-side fetching with tag revalidation.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cms",
|
|
7
|
+
"flexa",
|
|
8
|
+
"headless",
|
|
9
|
+
"nextjs",
|
|
10
|
+
"react-server-components"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/flexacms/sdk/tree/main/packages/next#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/flexacms/sdk/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/flexacms/sdk.git",
|
|
20
|
+
"directory": "packages/next"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"main": "./dist/index.cjs",
|
|
30
|
+
"module": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/index.d.cts",
|
|
40
|
+
"default": "./dist/index.cjs"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@flexacms/client": "^0.2.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"next": "^16.4.0",
|
|
56
|
+
"react": "^19.3.0",
|
|
57
|
+
"react-dom": "^19.3.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"next": ">=15.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|