@datum-cloud/strapi-revalidate 0.1.2 → 0.3.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/dist/cache/manager.d.ts +15 -2
- package/dist/cache/manager.d.ts.map +1 -1
- package/dist/cache/manager.js +28 -2
- package/dist/cache/manager.js.map +1 -1
- package/dist/client/graphql.d.ts +5 -2
- package/dist/client/graphql.d.ts.map +1 -1
- package/dist/client/graphql.js +1 -1
- package/dist/client/graphql.js.map +1 -1
- package/dist/client/rest.d.ts +1 -1
- package/dist/client/rest.d.ts.map +1 -1
- package/dist/client/rest.js.map +1 -1
- package/dist/types/config.d.ts +14 -3
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +8 -1
- package/dist/types/config.js.map +1 -1
- package/dist/webhook/event.d.ts +14 -0
- package/dist/webhook/event.d.ts.map +1 -0
- package/dist/webhook/event.js +28 -0
- package/dist/webhook/event.js.map +1 -0
- package/dist/webhook/handler.d.ts +3 -2
- package/dist/webhook/handler.d.ts.map +1 -1
- package/dist/webhook/handler.js +18 -12
- package/dist/webhook/handler.js.map +1 -1
- package/dist/webhook/index.d.ts +1 -0
- package/dist/webhook/index.d.ts.map +1 -1
- package/dist/webhook/index.js +1 -0
- package/dist/webhook/index.js.map +1 -1
- package/dist/webhook/tags.d.ts +5 -1
- package/dist/webhook/tags.d.ts.map +1 -1
- package/dist/webhook/tags.js +23 -4
- package/dist/webhook/tags.js.map +1 -1
- package/dist/webhook/types.d.ts +2 -2
- package/dist/webhook/types.d.ts.map +1 -1
- package/dist/webhook/types.js.map +1 -1
- package/package.json +1 -1
package/dist/cache/manager.d.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* - Track tag membership across writes so `invalidate(tag)` is O(tagged keys).
|
|
6
6
|
* - Mirror every successful `set()` to a persistent fallback driver with no
|
|
7
7
|
* TTL, so Strapi outages don't take the site down.
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
8
|
+
* - `getWithFallback()` for resilient read paths: primary hit → fetcher →
|
|
9
|
+
* fallback, with a single cache write on fresh data.
|
|
10
10
|
*
|
|
11
11
|
* Drivers themselves stay storage-only; tag accounting lives here.
|
|
12
12
|
*/
|
|
@@ -36,6 +36,19 @@ export declare class CacheManager {
|
|
|
36
36
|
get<T>(key: string): Promise<T | null>;
|
|
37
37
|
/** Read from the persistent fallback cache. Returns `null` if never populated. */
|
|
38
38
|
getFallback<T>(key: string): Promise<T | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Full read path with outage resilience:
|
|
41
|
+
* 1. Return the primary cache value if present.
|
|
42
|
+
* 2. Call `fetcher()` to get fresh data.
|
|
43
|
+
* 3. On success, write to both primary and fallback caches, then return.
|
|
44
|
+
* 4. On failure (`null`), return whatever the fallback cache holds.
|
|
45
|
+
*
|
|
46
|
+
* @param key Cache key.
|
|
47
|
+
* @param fetcher Async function that fetches fresh data. Return `null` to
|
|
48
|
+
* signal failure and trigger the fallback read.
|
|
49
|
+
* @param options TTL and tag options forwarded to `set()` on a fresh write.
|
|
50
|
+
*/
|
|
51
|
+
getWithFallback<T>(key: string, fetcher: () => Promise<T | null>, options?: CacheSetOptions): Promise<T | null>;
|
|
39
52
|
/**
|
|
40
53
|
* Write to both the primary cache (with TTL + tags) and the fallback cache
|
|
41
54
|
* (no TTL, no tags). Tag accounting happens at the driver level.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/cache/manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE/D,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,OAAO,EAAE,WAAW,CAAC;IACrB,mFAAmF;IACnF,QAAQ,EAAE,WAAW,CAAC;IACtB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;gBAEpB,OAAO,EAAE,mBAAmB;IAOxC,qEAAqE;IAC/D,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAQ5C,kFAAkF;IAC5E,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAIpD;;;OAGG;IACG,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOhF,mFAAmF;IAC7E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC;;;;OAIG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO5C,mEAAmE;IAC7D,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;CAMrC"}
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/cache/manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE/D,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,OAAO,EAAE,WAAW,CAAC;IACrB,mFAAmF;IACnF,QAAQ,EAAE,WAAW,CAAC;IACtB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;gBAEpB,OAAO,EAAE,mBAAmB;IAOxC,qEAAqE;IAC/D,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAQ5C,kFAAkF;IAC5E,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAIpD;;;;;;;;;;;OAWG;IACG,eAAe,CAAC,CAAC,EACrB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,EAChC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAgBpB;;;OAGG;IACG,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOhF,mFAAmF;IAC7E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC;;;;OAIG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO5C,mEAAmE;IAC7D,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;CAMrC"}
|
package/dist/cache/manager.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* - Track tag membership across writes so `invalidate(tag)` is O(tagged keys).
|
|
6
6
|
* - Mirror every successful `set()` to a persistent fallback driver with no
|
|
7
7
|
* TTL, so Strapi outages don't take the site down.
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
8
|
+
* - `getWithFallback()` for resilient read paths: primary hit → fetcher →
|
|
9
|
+
* fallback, with a single cache write on fresh data.
|
|
10
10
|
*
|
|
11
11
|
* Drivers themselves stay storage-only; tag accounting lives here.
|
|
12
12
|
*/
|
|
@@ -37,6 +37,32 @@ export class CacheManager {
|
|
|
37
37
|
async getFallback(key) {
|
|
38
38
|
return this.fallback.get(key);
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Full read path with outage resilience:
|
|
42
|
+
* 1. Return the primary cache value if present.
|
|
43
|
+
* 2. Call `fetcher()` to get fresh data.
|
|
44
|
+
* 3. On success, write to both primary and fallback caches, then return.
|
|
45
|
+
* 4. On failure (`null`), return whatever the fallback cache holds.
|
|
46
|
+
*
|
|
47
|
+
* @param key Cache key.
|
|
48
|
+
* @param fetcher Async function that fetches fresh data. Return `null` to
|
|
49
|
+
* signal failure and trigger the fallback read.
|
|
50
|
+
* @param options TTL and tag options forwarded to `set()` on a fresh write.
|
|
51
|
+
*/
|
|
52
|
+
async getWithFallback(key, fetcher, options = {}) {
|
|
53
|
+
const cached = await this.get(key);
|
|
54
|
+
if (cached !== null)
|
|
55
|
+
return cached;
|
|
56
|
+
const fresh = await fetcher();
|
|
57
|
+
if (fresh !== null) {
|
|
58
|
+
await this.set(key, fresh, options);
|
|
59
|
+
return fresh;
|
|
60
|
+
}
|
|
61
|
+
if (this.debug) {
|
|
62
|
+
console.debug(`[strapi-revalidate] fetcher returned null, reading fallback → ${key}`);
|
|
63
|
+
}
|
|
64
|
+
return this.getFallback(key);
|
|
65
|
+
}
|
|
40
66
|
/**
|
|
41
67
|
* Write to both the primary cache (with TTL + tags) and the fallback cache
|
|
42
68
|
* (no TTL, no tags). Tag accounting happens at the driver level.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/cache/manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAgBH;;;GAGG;AACH,MAAM,OAAO,YAAY;IACN,OAAO,CAAc;IACrB,QAAQ,CAAc;IACtB,UAAU,CAAS;IACnB,KAAK,CAAU;IAEhC,YAAY,OAA4B;QACtC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACjC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC;QAC7C,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC;IACtC,CAAC;IAED,qEAAqE;IACrE,KAAK,CAAC,GAAG,CAAI,GAAW;QACtB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAI,GAAG,CAAC,CAAC;QAC7C,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,6BAA6B,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,GAAG,EAAE,CAAC,CAAC;QACzF,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,kFAAkF;IAClF,KAAK,CAAC,WAAW,CAAI,GAAW;QAC9B,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAI,GAAG,CAAC,CAAC;IACnC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,GAAG,CAAI,GAAW,EAAE,IAAO,EAAE,UAA2B,EAAE;QAC9D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC;QAC3C,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/D,2EAA2E;QAC3E,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACrC,CAAC;IAED,mFAAmF;IACnF,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,UAAU,CAAC,GAAW;QAC1B,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,yCAAyC,GAAG,EAAE,CAAC,CAAC;QAChE,CAAC;IACH,CAAC;IAED,mEAAmE;IACnE,KAAK,CAAC,aAAa;QACjB,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAC3B,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;CACF","sourcesContent":["/**\n * CacheManager — the composition layer the public API talks to.\n *\n * Responsibilities:\n * - Track tag membership across writes so `invalidate(tag)` is O(tagged keys).\n * - Mirror every successful `set()` to a persistent fallback driver with no\n * TTL, so Strapi outages don't take the site down.\n * -
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/cache/manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * CacheManager — the composition layer the public API talks to.\n *\n * Responsibilities:\n * - Track tag membership across writes so `invalidate(tag)` is O(tagged keys).\n * - Mirror every successful `set()` to a persistent fallback driver with no\n * TTL, so Strapi outages don't take the site down.\n * - `getWithFallback()` for resilient read paths: primary hit → fetcher →\n * fallback, with a single cache write on fresh data.\n *\n * Drivers themselves stay storage-only; tag accounting lives here.\n */\n\nimport type { CacheDriver, CacheSetOptions } from './types.js';\n\n/** Options for instantiating a `CacheManager`. */\nexport interface CacheManagerOptions {\n /** Primary driver — honours TTL, serves hot reads. */\n primary: CacheDriver;\n /** Persistent fallback driver — never expires, written on every successful set. */\n fallback: CacheDriver;\n /** Default TTL (seconds) applied when `set` is called without one. */\n defaultTtl?: number;\n /** When true, hits/misses are logged via `console.debug`. */\n debug?: boolean;\n}\n\n/**\n * Cache facade. All content fetchers and the webhook handler talk to this,\n * not to a driver directly.\n */\nexport class CacheManager {\n private readonly primary: CacheDriver;\n private readonly fallback: CacheDriver;\n private readonly defaultTtl: number;\n private readonly debug: boolean;\n\n constructor(options: CacheManagerOptions) {\n this.primary = options.primary;\n this.fallback = options.fallback;\n this.defaultTtl = options.defaultTtl ?? 3600;\n this.debug = options.debug ?? false;\n }\n\n /** Read from the primary cache. Returns `null` on miss or expiry. */\n async get<T>(key: string): Promise<T | null> {\n const value = await this.primary.get<T>(key);\n if (this.debug) {\n console.debug(`[strapi-revalidate] cache ${value === null ? 'MISS' : 'HIT'} → ${key}`);\n }\n return value;\n }\n\n /** Read from the persistent fallback cache. Returns `null` if never populated. */\n async getFallback<T>(key: string): Promise<T | null> {\n return this.fallback.get<T>(key);\n }\n\n /**\n * Full read path with outage resilience:\n * 1. Return the primary cache value if present.\n * 2. Call `fetcher()` to get fresh data.\n * 3. On success, write to both primary and fallback caches, then return.\n * 4. On failure (`null`), return whatever the fallback cache holds.\n *\n * @param key Cache key.\n * @param fetcher Async function that fetches fresh data. Return `null` to\n * signal failure and trigger the fallback read.\n * @param options TTL and tag options forwarded to `set()` on a fresh write.\n */\n async getWithFallback<T>(\n key: string,\n fetcher: () => Promise<T | null>,\n options: CacheSetOptions = {}\n ): Promise<T | null> {\n const cached = await this.get<T>(key);\n if (cached !== null) return cached;\n\n const fresh = await fetcher();\n if (fresh !== null) {\n await this.set(key, fresh, options);\n return fresh;\n }\n\n if (this.debug) {\n console.debug(`[strapi-revalidate] fetcher returned null, reading fallback → ${key}`);\n }\n return this.getFallback<T>(key);\n }\n\n /**\n * Write to both the primary cache (with TTL + tags) and the fallback cache\n * (no TTL, no tags). Tag accounting happens at the driver level.\n */\n async set<T>(key: string, data: T, options: CacheSetOptions = {}): Promise<void> {\n const ttl = options.ttl ?? this.defaultTtl;\n await this.primary.set(key, data, { ttl, tags: options.tags });\n // Fallback: no TTL, no tag indexing — it exists purely to survive outages.\n await this.fallback.set(key, data);\n }\n\n /** Remove a single key from the primary cache. Fallback is preserved by design. */\n async delete(key: string): Promise<void> {\n await this.primary.delete(key);\n }\n\n /**\n * Invalidate every primary-cache key associated with `tag`. The fallback\n * cache is intentionally untouched — if the upcoming refetch fails, stale\n * data should still serve.\n */\n async invalidate(tag: string): Promise<void> {\n await this.primary.deleteByTag(tag);\n if (this.debug) {\n console.debug(`[strapi-revalidate] invalidated tag → ${tag}`);\n }\n }\n\n /** Invalidate all primary-cache entries. Fallback is preserved. */\n async invalidateAll(): Promise<void> {\n await this.primary.clear();\n if (this.debug) {\n console.debug('[strapi-revalidate] invalidated all');\n }\n }\n}\n"]}
|
package/dist/client/graphql.d.ts
CHANGED
|
@@ -16,8 +16,11 @@ export interface GraphQLResponse<T> {
|
|
|
16
16
|
export interface GraphQLClientOptions {
|
|
17
17
|
/** Strapi base URL (no trailing `/graphql`). */
|
|
18
18
|
url: string;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Strapi API token. Sent as `Authorization: Bearer <token>` when present.
|
|
21
|
+
* Omit for Strapi instances where the queried content is publicly readable.
|
|
22
|
+
*/
|
|
23
|
+
token?: string;
|
|
21
24
|
/** Per-request timeout in **milliseconds**. */
|
|
22
25
|
timeout: number;
|
|
23
26
|
/** Number of retries on transport failure (timeout, network, 5xx). 0 disables retry. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../../src/client/graphql.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,gDAAgD;AAChD,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrC;AAED,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACnC,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ
|
|
1
|
+
{"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../../src/client/graphql.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,gDAAgD;AAChD,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrC;AAED,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACnC,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,wFAAwF;IACxF,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;GAMG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;gBAEnC,OAAO,EAAE,oBAAoB;IAIzC;;;;OAIG;IACG,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;YAiC3E,WAAW;CAwC1B"}
|
package/dist/client/graphql.js
CHANGED
|
@@ -60,7 +60,7 @@ export class GraphQLStrapiClient {
|
|
|
60
60
|
method: 'POST',
|
|
61
61
|
headers: {
|
|
62
62
|
'Content-Type': 'application/json',
|
|
63
|
-
Authorization: `Bearer ${this.options.token}
|
|
63
|
+
...(this.options.token ? { Authorization: `Bearer ${this.options.token}` } : {}),
|
|
64
64
|
},
|
|
65
65
|
body: JSON.stringify({ query, variables }),
|
|
66
66
|
signal: controller.signal,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphql.js","sourceRoot":"","sources":["../../src/client/graphql.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"graphql.js","sourceRoot":"","sources":["../../src/client/graphql.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GraphQL transport for Strapi v5.\n *\n * Adapted from datum.net's `graphqlQuery()` with two additions: configurable\n * retry with exponential backoff, and a structured `StrapiClient` surface so\n * the same instance is reused across fetchers (no module-level state).\n */\n\n/** Generic Strapi GraphQL response envelope. */\nexport interface GraphQLResponse<T> {\n data: T;\n errors?: Array<{ message: string }>;\n}\n\n/** Configuration for a `GraphQLStrapiClient`. */\nexport interface GraphQLClientOptions {\n /** Strapi base URL (no trailing `/graphql`). */\n url: string;\n /**\n * Strapi API token. Sent as `Authorization: Bearer <token>` when present.\n * Omit for Strapi instances where the queried content is publicly readable.\n */\n token?: string;\n /** Per-request timeout in **milliseconds**. */\n timeout: number;\n /** Number of retries on transport failure (timeout, network, 5xx). 0 disables retry. */\n retry: number;\n /** When true, logs request lifecycle via `console.debug`. */\n debug?: boolean;\n}\n\n/**\n * Strapi GraphQL client.\n *\n * `query<T>()` returns the unwrapped `data` payload or `null` on failure;\n * callers are expected to treat `null` as \"Strapi unreachable / invalid\"\n * and fall back to stale cache.\n */\nexport class GraphQLStrapiClient {\n private readonly options: GraphQLClientOptions;\n\n constructor(options: GraphQLClientOptions) {\n this.options = options;\n }\n\n /**\n * Execute a GraphQL operation. Returns the `data` payload on success,\n * `null` if the server returns an error envelope, a non-2xx status,\n * times out, or all retries are exhausted.\n */\n async query<T>(query: string, variables: Record<string, unknown> = {}): Promise<T | null> {\n const { retry } = this.options;\n let lastError: unknown = null;\n\n for (let attempt = 0; attempt <= retry; attempt++) {\n try {\n const result = await this.executeOnce<T>(query, variables);\n if (result !== null) return result;\n // result === null means a structured Strapi error (4xx / GraphQL errors).\n // Don't retry those — they won't get better.\n return null;\n } catch (err) {\n lastError = err;\n if (this.options.debug) {\n console.debug(\n `[strapi-revalidate] graphql attempt ${attempt + 1}/${retry + 1} failed:`,\n err\n );\n }\n if (attempt < retry) {\n await sleep(backoffMs(attempt));\n }\n }\n }\n\n if (lastError instanceof Error && lastError.name === 'AbortError') {\n console.error(`Strapi request timed out after ${this.options.timeout}ms`);\n } else if (lastError) {\n console.error('Error fetching from Strapi:', lastError);\n }\n return null;\n }\n\n private async executeOnce<T>(\n query: string,\n variables: Record<string, unknown>\n ): Promise<T | null> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.options.timeout);\n\n try {\n const response = await fetch(`${this.options.url.replace(/\\/$/, '')}/graphql`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(this.options.token ? { Authorization: `Bearer ${this.options.token}` } : {}),\n },\n body: JSON.stringify({ query, variables }),\n signal: controller.signal,\n });\n\n if (!response.ok) {\n const text = await response.text();\n console.error(`Strapi GraphQL error: ${response.status} ${response.statusText}`, text);\n // 5xx is retryable — surface as a thrown error so the retry loop fires.\n if (response.status >= 500) {\n throw new Error(`Strapi ${response.status} ${response.statusText}`);\n }\n return null;\n }\n\n const result = (await response.json()) as GraphQLResponse<T>;\n\n if (result.errors && result.errors.length > 0) {\n console.error('Strapi GraphQL errors:', result.errors);\n return null;\n }\n\n return result.data;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Exponential backoff with a sane cap. Attempt is 0-indexed. */\nfunction backoffMs(attempt: number): number {\n const base = 200 * 2 ** attempt;\n return Math.min(base, 2000);\n}\n"]}
|
package/dist/client/rest.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/client/rest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/client/rest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAKD,4FAA4F;AAC5F,qBAAa,gBAAgB;IAE3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;gBAEhC,OAAO,EAAE,iBAAiB;IAIhC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;CAIlF;AAED,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,gBAAgB,CAE9E"}
|
package/dist/client/rest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rest.js","sourceRoot":"","sources":["../../src/client/rest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,MAAM,uBAAuB,GAC3B,+FAA+F,CAAC;AAElG,4FAA4F;AAC5F,MAAM,OAAO,gBAAgB;IAC3B,iEAAiE;IAChD,OAAO,CAAoB;IAE5C,YAAY,OAA0B;QACpC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,GAAG,CAAI,KAAa,EAAE,OAAiC;QAC3D,KAAK,IAAI,CAAC,OAAO,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC3C,CAAC;CACF;AAED,4EAA4E;AAC5E,MAAM,UAAU,gBAAgB,CAAC,QAA2B;IAC1D,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;AAC3C,CAAC","sourcesContent":["/**\n * REST transport for Strapi — STUB.\n *\n * The interface is exported for forward compatibility; runtime usage throws\n * a clear error. v0.1.0 only ships the GraphQL transport because every\n * built-in content fetcher uses GraphQL.\n */\n\n/** Configuration for the (not-yet-implemented) REST client. */\nexport interface RestClientOptions {\n url: string;\n token
|
|
1
|
+
{"version":3,"file":"rest.js","sourceRoot":"","sources":["../../src/client/rest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,MAAM,uBAAuB,GAC3B,+FAA+F,CAAC;AAElG,4FAA4F;AAC5F,MAAM,OAAO,gBAAgB;IAC3B,iEAAiE;IAChD,OAAO,CAAoB;IAE5C,YAAY,OAA0B;QACpC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,GAAG,CAAI,KAAa,EAAE,OAAiC;QAC3D,KAAK,IAAI,CAAC,OAAO,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAC3C,CAAC;CACF;AAED,4EAA4E;AAC5E,MAAM,UAAU,gBAAgB,CAAC,QAA2B;IAC1D,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;AAC3C,CAAC","sourcesContent":["/**\n * REST transport for Strapi — STUB.\n *\n * The interface is exported for forward compatibility; runtime usage throws\n * a clear error. v0.1.0 only ships the GraphQL transport because every\n * built-in content fetcher uses GraphQL.\n */\n\n/** Configuration for the (not-yet-implemented) REST client. */\nexport interface RestClientOptions {\n url: string;\n token?: string;\n timeout: number;\n retry: number;\n}\n\nconst NOT_IMPLEMENTED_MESSAGE =\n 'REST transport not yet implemented. Use transport: \"graphql\" or pin to a future v1.x release.';\n\n/** Stubbed REST client. Constructor is callable so config typing works; all reads throw. */\nexport class RestStrapiClient {\n // Stored to keep strict property init quiet and document intent.\n private readonly options: RestClientOptions;\n\n constructor(options: RestClientOptions) {\n this.options = options;\n }\n\n async get<T>(_path: string, _params?: Record<string, unknown>): Promise<T | null> {\n void this.options;\n throw new Error(NOT_IMPLEMENTED_MESSAGE);\n }\n}\n\n/** Factory that mirrors `createStrapiClient` shape. Throws on first use. */\nexport function createRestClient(_options: RestClientOptions): RestStrapiClient {\n throw new Error(NOT_IMPLEMENTED_MESSAGE);\n}\n"]}
|
package/dist/types/config.d.ts
CHANGED
|
@@ -20,12 +20,20 @@ export type TransportName = z.infer<typeof transportNameSchema>;
|
|
|
20
20
|
*/
|
|
21
21
|
export declare const revalidateConfigSchema: z.ZodObject<{
|
|
22
22
|
url: z.ZodString;
|
|
23
|
-
token: z.ZodString
|
|
23
|
+
token: z.ZodOptional<z.ZodString>;
|
|
24
24
|
webhook: z.ZodOptional<z.ZodObject<{
|
|
25
25
|
secret: z.ZodOptional<z.ZodString>;
|
|
26
26
|
tagMap: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>>;
|
|
27
27
|
onRevalidate: z.ZodOptional<z.ZodType<(event: WebhookEvent) => void | Promise<void>, z.ZodTypeDef, (event: WebhookEvent) => void | Promise<void>>>;
|
|
28
|
+
/**
|
|
29
|
+
* When true (default), an `onRevalidate` hook failure returns HTTP 502 with
|
|
30
|
+
* `{ ok: false, invalidated: true }` so webhook callers can detect partial
|
|
31
|
+
* failure after cache invalidation. Set false to preserve the pre-0.3.0
|
|
32
|
+
* behaviour of logging the error and returning 200.
|
|
33
|
+
*/
|
|
34
|
+
failOnWarmError: z.ZodDefault<z.ZodBoolean>;
|
|
28
35
|
}, "strip", z.ZodTypeAny, {
|
|
36
|
+
failOnWarmError: boolean;
|
|
29
37
|
secret?: string | undefined;
|
|
30
38
|
tagMap?: Record<string, string[]> | undefined;
|
|
31
39
|
onRevalidate?: ((event: WebhookEvent) => void | Promise<void>) | undefined;
|
|
@@ -33,6 +41,7 @@ export declare const revalidateConfigSchema: z.ZodObject<{
|
|
|
33
41
|
secret?: string | undefined;
|
|
34
42
|
tagMap?: Record<string, string[]> | undefined;
|
|
35
43
|
onRevalidate?: ((event: WebhookEvent) => void | Promise<void>) | undefined;
|
|
44
|
+
failOnWarmError?: boolean | undefined;
|
|
36
45
|
}>>;
|
|
37
46
|
cache: z.ZodDefault<z.ZodObject<{
|
|
38
47
|
driver: z.ZodDefault<z.ZodEnum<["file", "memory", "redis"]>>;
|
|
@@ -62,7 +71,6 @@ export declare const revalidateConfigSchema: z.ZodObject<{
|
|
|
62
71
|
debug: z.ZodDefault<z.ZodBoolean>;
|
|
63
72
|
}, "strip", z.ZodTypeAny, {
|
|
64
73
|
url: string;
|
|
65
|
-
token: string;
|
|
66
74
|
cache: {
|
|
67
75
|
driver: "file" | "memory" | "redis";
|
|
68
76
|
ttl: number;
|
|
@@ -75,18 +83,21 @@ export declare const revalidateConfigSchema: z.ZodObject<{
|
|
|
75
83
|
timeout: number;
|
|
76
84
|
retry: number;
|
|
77
85
|
debug: boolean;
|
|
86
|
+
token?: string | undefined;
|
|
78
87
|
webhook?: {
|
|
88
|
+
failOnWarmError: boolean;
|
|
79
89
|
secret?: string | undefined;
|
|
80
90
|
tagMap?: Record<string, string[]> | undefined;
|
|
81
91
|
onRevalidate?: ((event: WebhookEvent) => void | Promise<void>) | undefined;
|
|
82
92
|
} | undefined;
|
|
83
93
|
}, {
|
|
84
94
|
url: string;
|
|
85
|
-
token
|
|
95
|
+
token?: string | undefined;
|
|
86
96
|
webhook?: {
|
|
87
97
|
secret?: string | undefined;
|
|
88
98
|
tagMap?: Record<string, string[]> | undefined;
|
|
89
99
|
onRevalidate?: ((event: WebhookEvent) => void | Promise<void>) | undefined;
|
|
100
|
+
failOnWarmError?: boolean | undefined;
|
|
90
101
|
} | undefined;
|
|
91
102
|
cache?: {
|
|
92
103
|
driver?: "file" | "memory" | "redis" | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,0CAA0C;AAC1C,eAAO,MAAM,qBAAqB,wCAAsC,CAAC;AACzE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,6DAA6D;AAC7D,eAAO,MAAM,mBAAmB,gCAA8B,CAAC;AAC/D,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,0CAA0C;AAC1C,eAAO,MAAM,qBAAqB,wCAAsC,CAAC;AACzE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,6DAA6D;AAC7D,eAAO,MAAM,mBAAmB,gCAA8B,CAAC;AAC/D,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAoChE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB;;;;;;sDApBb,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,wBAArC,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QAIvD;;;;;WAKG;;;;;;gCATe,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;;;gCAArC,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAArC,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;;;;;;;gCAArC,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;EA6BzD,CAAC;AAEH,oDAAoD;AACpD,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE3E,iDAAiD;AACjD,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
|
package/dist/types/config.js
CHANGED
|
@@ -30,6 +30,13 @@ const webhookConfigSchema = z
|
|
|
30
30
|
onRevalidate: z
|
|
31
31
|
.custom((val) => typeof val === 'function')
|
|
32
32
|
.optional(),
|
|
33
|
+
/**
|
|
34
|
+
* When true (default), an `onRevalidate` hook failure returns HTTP 502 with
|
|
35
|
+
* `{ ok: false, invalidated: true }` so webhook callers can detect partial
|
|
36
|
+
* failure after cache invalidation. Set false to preserve the pre-0.3.0
|
|
37
|
+
* behaviour of logging the error and returning 200.
|
|
38
|
+
*/
|
|
39
|
+
failOnWarmError: z.boolean().default(true),
|
|
33
40
|
})
|
|
34
41
|
.optional();
|
|
35
42
|
/**
|
|
@@ -40,7 +47,7 @@ const webhookConfigSchema = z
|
|
|
40
47
|
*/
|
|
41
48
|
export const revalidateConfigSchema = z.object({
|
|
42
49
|
url: z.string().url(),
|
|
43
|
-
token: z.string().min(1),
|
|
50
|
+
token: z.string().min(1).optional(),
|
|
44
51
|
webhook: webhookConfigSchema,
|
|
45
52
|
cache: cacheConfigSchema,
|
|
46
53
|
transport: transportNameSchema.default('graphql'),
|
package/dist/types/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,0CAA0C;AAC1C,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAGzE,6DAA6D;AAC7D,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;AAG/D,MAAM,iBAAiB,GAAG,CAAC;KACxB,MAAM,CAAC;IACN,MAAM,EAAE,qBAAqB,CAAC,OAAO,CAAC,MAAM,CAAC;IAC7C,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAC9C,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,0BAA0B,CAAC;IACnD,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,wBAAwB,CAAC;IACzD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACrC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC;KACD,OAAO,CAAC,EAAE,CAAC,CAAC;AAEf,MAAM,mBAAmB,GAAG,CAAC;KAC1B,MAAM,CAAC;IACN,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE;IAChD,6EAA6E;IAC7E,+EAA+E;IAC/E,+EAA+E;IAC/E,iEAAiE;IACjE,YAAY,EAAE,CAAC;SACZ,MAAM,CACL,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,GAAG,KAAK,UAAU,CACnC;SACA,QAAQ,EAAE;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,0CAA0C;AAC1C,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;AAGzE,6DAA6D;AAC7D,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;AAG/D,MAAM,iBAAiB,GAAG,CAAC;KACxB,MAAM,CAAC;IACN,MAAM,EAAE,qBAAqB,CAAC,OAAO,CAAC,MAAM,CAAC;IAC7C,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAC9C,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,0BAA0B,CAAC;IACnD,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,wBAAwB,CAAC;IACzD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACrC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC;KACD,OAAO,CAAC,EAAE,CAAC,CAAC;AAEf,MAAM,mBAAmB,GAAG,CAAC;KAC1B,MAAM,CAAC;IACN,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE;IAChD,6EAA6E;IAC7E,+EAA+E;IAC/E,+EAA+E;IAC/E,iEAAiE;IACjE,YAAY,EAAE,CAAC;SACZ,MAAM,CACL,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,GAAG,KAAK,UAAU,CACnC;SACA,QAAQ,EAAE;IACb;;;;;OAKG;IACH,eAAe,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;CAC3C,CAAC;KACD,QAAQ,EAAE,CAAC;AAEd;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACrB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACnC,OAAO,EAAE,mBAAmB;IAC5B,KAAK,EAAE,iBAAiB;IACxB,SAAS,EAAE,mBAAmB,CAAC,OAAO,CAAC,SAAS,CAAC;IACjD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAClD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACzC,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CAClC,CAAC,CAAC","sourcesContent":["/**\n * Runtime configuration schema for strapi-revalidate.\n *\n * Validated with zod so misconfigurations surface at startup rather than\n * at the first cache miss or webhook delivery.\n */\n\nimport { z } from 'zod';\nimport type { WebhookEvent } from '../webhook/types.js';\n\n/** Supported cache driver identifiers. */\nexport const cacheDriverNameSchema = z.enum(['file', 'memory', 'redis']);\nexport type CacheDriverName = z.infer<typeof cacheDriverNameSchema>;\n\n/** Supported transport identifiers for the Strapi client. */\nexport const transportNameSchema = z.enum(['graphql', 'rest']);\nexport type TransportName = z.infer<typeof transportNameSchema>;\n\nconst cacheConfigSchema = z\n .object({\n driver: cacheDriverNameSchema.default('file'),\n ttl: z.number().int().positive().default(3600),\n dir: z.string().default('.cache/strapi-revalidate'),\n fallbackDir: z.string().default('.cache/strapi-fallback'),\n redisUrl: z.string().url().optional(),\n keyPrefix: z.string().optional(),\n })\n .default({});\n\nconst webhookConfigSchema = z\n .object({\n secret: z.string().optional(),\n tagMap: z.record(z.array(z.string())).optional(),\n // `z.function().args(...).returns(...)` is a zod v3-only chain — v4 reshaped\n // the function schema API and removed it. Use `z.custom` instead so the schema\n // loads under either major version. Runtime validation still ensures the value\n // is a function; the TS-level signature carries the typed shape.\n onRevalidate: z\n .custom<(event: WebhookEvent) => void | Promise<void>>(\n (val) => typeof val === 'function'\n )\n .optional(),\n /**\n * When true (default), an `onRevalidate` hook failure returns HTTP 502 with\n * `{ ok: false, invalidated: true }` so webhook callers can detect partial\n * failure after cache invalidation. Set false to preserve the pre-0.3.0\n * behaviour of logging the error and returning 200.\n */\n failOnWarmError: z.boolean().default(true),\n })\n .optional();\n\n/**\n * Full runtime configuration schema.\n *\n * Use `revalidateConfigSchema.parse(input)` to validate user input and\n * apply defaults in one step.\n */\nexport const revalidateConfigSchema = z.object({\n url: z.string().url(),\n token: z.string().min(1).optional(),\n webhook: webhookConfigSchema,\n cache: cacheConfigSchema,\n transport: transportNameSchema.default('graphql'),\n timeout: z.number().int().positive().default(3000),\n retry: z.number().int().min(0).default(2),\n debug: z.boolean().default(false),\n});\n\n/** User-supplied configuration (pre-validation). */\nexport type RevalidateConfigInput = z.input<typeof revalidateConfigSchema>;\n\n/** Validated, defaults-applied configuration. */\nexport type RevalidateConfig = z.output<typeof revalidateConfigSchema>;\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for normalizing Strapi webhook payloads into {@link WebhookEvent}.
|
|
3
|
+
*/
|
|
4
|
+
import type { WebhookEvent, WebhookPayload } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a stable entry identifier from a webhook entry payload.
|
|
7
|
+
* Strapi v5 usually sends `documentId`; older payloads may only include numeric `id`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveEntryId(entry: WebhookPayload['entry']): string;
|
|
10
|
+
/**
|
|
11
|
+
* Build the normalized event passed to `onRevalidate` after tag resolution.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildWebhookEvent(payload: WebhookPayload, tags: string[]): WebhookEvent;
|
|
14
|
+
//# sourceMappingURL=event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/webhook/event.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE/D;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,MAAM,CAIrE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,YAAY,CASvF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for normalizing Strapi webhook payloads into {@link WebhookEvent}.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a stable entry identifier from a webhook entry payload.
|
|
6
|
+
* Strapi v5 usually sends `documentId`; older payloads may only include numeric `id`.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveEntryId(entry) {
|
|
9
|
+
if (typeof entry.documentId === 'string')
|
|
10
|
+
return entry.documentId;
|
|
11
|
+
if (typeof entry.id === 'number')
|
|
12
|
+
return String(entry.id);
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build the normalized event passed to `onRevalidate` after tag resolution.
|
|
17
|
+
*/
|
|
18
|
+
export function buildWebhookEvent(payload, tags) {
|
|
19
|
+
return {
|
|
20
|
+
event: payload.event,
|
|
21
|
+
model: payload.model,
|
|
22
|
+
uid: payload.uid,
|
|
23
|
+
entryId: resolveEntryId(payload.entry),
|
|
24
|
+
slug: typeof payload.entry.slug === 'string' ? payload.entry.slug : undefined,
|
|
25
|
+
tags,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=event.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event.js","sourceRoot":"","sources":["../../src/webhook/event.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,KAA8B;IAC3D,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,UAAU,CAAC;IAClE,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC1D,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAuB,EAAE,IAAc;IACvE,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC;QACtC,IAAI,EAAE,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;QAC7E,IAAI;KACL,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Helpers for normalizing Strapi webhook payloads into {@link WebhookEvent}.\n */\n\nimport type { WebhookEvent, WebhookPayload } from './types.js';\n\n/**\n * Resolve a stable entry identifier from a webhook entry payload.\n * Strapi v5 usually sends `documentId`; older payloads may only include numeric `id`.\n */\nexport function resolveEntryId(entry: WebhookPayload['entry']): string {\n if (typeof entry.documentId === 'string') return entry.documentId;\n if (typeof entry.id === 'number') return String(entry.id);\n return '';\n}\n\n/**\n * Build the normalized event passed to `onRevalidate` after tag resolution.\n */\nexport function buildWebhookEvent(payload: WebhookPayload, tags: string[]): WebhookEvent {\n return {\n event: payload.event,\n model: payload.model,\n uid: payload.uid,\n entryId: resolveEntryId(payload.entry),\n slug: typeof payload.entry.slug === 'string' ? payload.entry.slug : undefined,\n tags,\n };\n}\n"]}
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
* 1. 405 if method isn't POST.
|
|
12
12
|
* 2. 401 if a secret is configured and the request's secret doesn't match
|
|
13
13
|
* (constant-time compare; supports `Authorization: Bearer <secret>`,
|
|
14
|
-
* `X-Strapi-Signature`,
|
|
14
|
+
* `X-Strapi-Signature`, `strapi-webhook-secret`, or `X-Webhook-Secret`).
|
|
15
15
|
* 3. 400 if the body isn't a parseable Strapi v5 webhook payload.
|
|
16
16
|
* 4. Resolves tags via `mapModelToTags`, calls `cache.invalidate(tag)` for
|
|
17
17
|
* each, and invokes `onRevalidate` if configured.
|
|
18
|
-
* 5. 200 with `{ ok: true, tags }
|
|
18
|
+
* 5. 200 with `{ ok: true, tags }`, or 502 when `onRevalidate` throws and
|
|
19
|
+
* `webhook.failOnWarmError` is true (default).
|
|
19
20
|
*/
|
|
20
21
|
import type { CacheManager } from '../cache/manager.js';
|
|
21
22
|
import type { RevalidateConfig, RevalidateConfigInput } from '../types/config.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/webhook/handler.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/webhook/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAGlF,OAAO,KAAK,EAAkB,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElF,+DAA+D;AAC/D,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1F;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,qBAAqB,GAAG,cAAc,CAAC;AACnF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,YAAY,CAAC;CACrB,GAAG,cAAc,CAAC"}
|
package/dist/webhook/handler.js
CHANGED
|
@@ -11,15 +11,17 @@
|
|
|
11
11
|
* 1. 405 if method isn't POST.
|
|
12
12
|
* 2. 401 if a secret is configured and the request's secret doesn't match
|
|
13
13
|
* (constant-time compare; supports `Authorization: Bearer <secret>`,
|
|
14
|
-
* `X-Strapi-Signature`,
|
|
14
|
+
* `X-Strapi-Signature`, `strapi-webhook-secret`, or `X-Webhook-Secret`).
|
|
15
15
|
* 3. 400 if the body isn't a parseable Strapi v5 webhook payload.
|
|
16
16
|
* 4. Resolves tags via `mapModelToTags`, calls `cache.invalidate(tag)` for
|
|
17
17
|
* each, and invokes `onRevalidate` if configured.
|
|
18
|
-
* 5. 200 with `{ ok: true, tags }
|
|
18
|
+
* 5. 200 with `{ ok: true, tags }`, or 502 when `onRevalidate` throws and
|
|
19
|
+
* `webhook.failOnWarmError` is true (default).
|
|
19
20
|
*/
|
|
20
21
|
import { timingSafeEqual } from 'node:crypto';
|
|
21
22
|
import { createCache } from '../cache/index.js';
|
|
22
23
|
import { revalidateConfigSchema } from '../types/config.js';
|
|
24
|
+
import { buildWebhookEvent } from './event.js';
|
|
23
25
|
import { mapModelToTags } from './tags.js';
|
|
24
26
|
export function createWebhookHandler(input) {
|
|
25
27
|
const { config, cache } = resolveDeps(input);
|
|
@@ -54,22 +56,25 @@ export function createWebhookHandler(input) {
|
|
|
54
56
|
for (const tag of tags) {
|
|
55
57
|
await cache.invalidate(tag);
|
|
56
58
|
}
|
|
57
|
-
const event =
|
|
58
|
-
|
|
59
|
-
model: payload.model,
|
|
60
|
-
uid: payload.uid,
|
|
61
|
-
entryId: payload.entry.documentId,
|
|
62
|
-
slug: typeof payload.entry.slug === 'string' ? payload.entry.slug : undefined,
|
|
63
|
-
tags,
|
|
64
|
-
};
|
|
59
|
+
const event = buildWebhookEvent(payload, tags);
|
|
60
|
+
const failOnWarmError = config.webhook?.failOnWarmError ?? true;
|
|
65
61
|
if (config.webhook?.onRevalidate) {
|
|
66
62
|
try {
|
|
67
63
|
await config.webhook.onRevalidate(event);
|
|
68
64
|
}
|
|
69
65
|
catch (err) {
|
|
70
|
-
// The cache is already invalidated; surface the hook failure to the caller
|
|
71
|
-
// but treat the revalidation itself as successful.
|
|
72
66
|
console.error('[strapi-revalidate] onRevalidate hook threw:', err);
|
|
67
|
+
if (failOnWarmError) {
|
|
68
|
+
res.status(502);
|
|
69
|
+
res.json({
|
|
70
|
+
ok: false,
|
|
71
|
+
invalidated: true,
|
|
72
|
+
error: 'Cache warm failed',
|
|
73
|
+
message: err instanceof Error ? err.message : 'Unknown warm error',
|
|
74
|
+
tags,
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
res.status(200);
|
|
@@ -100,6 +105,7 @@ function readSecret(req) {
|
|
|
100
105
|
}
|
|
101
106
|
return (headerValue(req, 'x-strapi-signature') ??
|
|
102
107
|
headerValue(req, 'strapi-webhook-secret') ??
|
|
108
|
+
headerValue(req, 'x-webhook-secret') ??
|
|
103
109
|
null);
|
|
104
110
|
}
|
|
105
111
|
function headerValue(req, name) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/webhook/handler.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Framework-agnostic webhook handler.\n *\n * `createWebhookHandler(config)` returns an `async (req, res) => void`\n * function whose `req` and `res` accept the lowest-common-denominator\n * shapes used by Astro, Next.js (Route Handlers), SvelteKit, Express,\n * and raw Node http. Each framework's docs page in the README shows the\n * single line of glue code needed.\n *\n * Behaviour:\n * 1. 405 if method isn't POST.\n * 2. 401 if a secret is configured and the request's secret doesn't match\n * (constant-time compare; supports `Authorization: Bearer <secret>`,\n * `X-Strapi-Signature`, or a raw `strapi-webhook-secret` header).\n * 3. 400 if the body isn't a parseable Strapi v5 webhook payload.\n * 4. Resolves tags via `mapModelToTags`, calls `cache.invalidate(tag)` for\n * each, and invokes `onRevalidate` if configured.\n * 5. 200 with `{ ok: true, tags }`.\n */\n\nimport { timingSafeEqual } from 'node:crypto';\nimport { createCache } from '../cache/index.js';\nimport type { CacheManager } from '../cache/manager.js';\nimport { revalidateConfigSchema } from '../types/config.js';\nimport type { RevalidateConfig, RevalidateConfigInput } from '../types/config.js';\nimport { mapModelToTags } from './tags.js';\nimport type {\n WebhookEvent,\n WebhookPayload,\n WebhookRequest,\n WebhookResponse,\n} from './types.js';\n\n/** Async function shape returned by `createWebhookHandler`. */\nexport type WebhookHandler = (req: WebhookRequest, res: WebhookResponse) => Promise<void>;\n\n/**\n * Build a webhook handler bound to a validated config and the cache\n * manager derived from it.\n *\n * Accepts either a `RevalidateConfigInput` (raw user object — gets validated)\n * or a pair of pre-built dependencies, so power users can share a single\n * `CacheManager` across handlers and content fetchers.\n */\nexport function createWebhookHandler(input: RevalidateConfigInput): WebhookHandler;\nexport function createWebhookHandler(deps: {\n config: RevalidateConfig;\n cache: CacheManager;\n}): WebhookHandler;\nexport function createWebhookHandler(\n input: RevalidateConfigInput | { config: RevalidateConfig; cache: CacheManager }\n): WebhookHandler {\n const { config, cache } = resolveDeps(input);\n\n return async function handle(req, res) {\n if (req.method && req.method.toUpperCase() !== 'POST') {\n res.status(405);\n res.json({ ok: false, error: 'Method not allowed' });\n return;\n }\n\n const configuredSecret = config.webhook?.secret;\n if (configuredSecret) {\n const provided = readSecret(req);\n if (!provided || !constantTimeEqual(provided, configuredSecret)) {\n res.status(401);\n res.json({ ok: false, error: 'Invalid secret' });\n return;\n }\n }\n\n let payload: WebhookPayload;\n try {\n payload = await readPayload(req);\n } catch (err) {\n res.status(400);\n res.json({\n ok: false,\n error: err instanceof Error ? err.message : 'Bad payload',\n });\n return;\n }\n\n const tags = mapModelToTags(payload, config.webhook?.tagMap);\n for (const tag of tags) {\n await cache.invalidate(tag);\n }\n\n const event: WebhookEvent = {\n event: payload.event,\n model: payload.model,\n uid: payload.uid,\n entryId: payload.entry.documentId,\n slug: typeof payload.entry.slug === 'string' ? payload.entry.slug : undefined,\n tags,\n };\n\n if (config.webhook?.onRevalidate) {\n try {\n await config.webhook.onRevalidate(event);\n } catch (err) {\n // The cache is already invalidated; surface the hook failure to the caller\n // but treat the revalidation itself as successful.\n console.error('[strapi-revalidate] onRevalidate hook threw:', err);\n }\n }\n\n res.status(200);\n res.json({ ok: true, tags });\n };\n}\n\nfunction resolveDeps(\n input: RevalidateConfigInput | { config: RevalidateConfig; cache: CacheManager }\n): { config: RevalidateConfig; cache: CacheManager } {\n if (isPreBuilt(input)) return input;\n const config = revalidateConfigSchema.parse(input);\n const cache = createCache(config);\n return { config, cache };\n}\n\nfunction isPreBuilt(\n value: unknown\n): value is { config: RevalidateConfig; cache: CacheManager } {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'config' in value &&\n 'cache' in value &&\n typeof (value as { cache: unknown }).cache === 'object'\n );\n}\n\nfunction readSecret(req: WebhookRequest): string | null {\n // Headers can arrive as Fetch-style Headers (`.get()`) or as a plain map.\n const direct = headerValue(req, 'authorization');\n if (direct) {\n const match = /^Bearer\\s+(.+)$/i.exec(direct);\n if (match) return match[1].trim();\n }\n\n return (\n headerValue(req, 'x-strapi-signature') ??\n headerValue(req, 'strapi-webhook-secret') ??\n null\n );\n}\n\nfunction headerValue(req: WebhookRequest, name: string): string | null {\n const headers = req.headers;\n if (typeof headers.get === 'function') {\n return headers.get(name);\n }\n const lower = name.toLowerCase();\n const raw = (headers as Record<string, unknown>)[lower] ?? (headers as Record<string, unknown>)[name];\n if (typeof raw === 'string') return raw;\n if (Array.isArray(raw) && typeof raw[0] === 'string') return raw[0];\n return null;\n}\n\nasync function readPayload(req: WebhookRequest): Promise<WebhookPayload> {\n let raw: unknown;\n if (typeof req.json === 'function') {\n raw = await req.json();\n } else if (typeof req.text === 'function') {\n const text = await req.text();\n raw = text.length > 0 ? JSON.parse(text) : {};\n } else {\n throw new Error('Request object exposes neither json() nor text()');\n }\n\n if (!isWebhookPayload(raw)) {\n throw new Error('Payload is not a valid Strapi v5 webhook event');\n }\n return raw;\n}\n\nfunction isWebhookPayload(value: unknown): value is WebhookPayload {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.event !== 'string' || typeof v.model !== 'string' || typeof v.uid !== 'string') {\n return false;\n }\n if (typeof v.entry !== 'object' || v.entry === null) return false;\n const entry = v.entry as Record<string, unknown>;\n return typeof entry.documentId === 'string' || typeof entry.id === 'number';\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n // timingSafeEqual requires equal-length buffers; pad both to the longer length so the\n // length check itself is constant-time-safe (different lengths still return false).\n const bufA = Buffer.from(a, 'utf-8');\n const bufB = Buffer.from(b, 'utf-8');\n if (bufA.length !== bufB.length) return false;\n return timingSafeEqual(bufA, bufB);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/webhook/handler.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Framework-agnostic webhook handler.\n *\n * `createWebhookHandler(config)` returns an `async (req, res) => void`\n * function whose `req` and `res` accept the lowest-common-denominator\n * shapes used by Astro, Next.js (Route Handlers), SvelteKit, Express,\n * and raw Node http. Each framework's docs page in the README shows the\n * single line of glue code needed.\n *\n * Behaviour:\n * 1. 405 if method isn't POST.\n * 2. 401 if a secret is configured and the request's secret doesn't match\n * (constant-time compare; supports `Authorization: Bearer <secret>`,\n * `X-Strapi-Signature`, `strapi-webhook-secret`, or `X-Webhook-Secret`).\n * 3. 400 if the body isn't a parseable Strapi v5 webhook payload.\n * 4. Resolves tags via `mapModelToTags`, calls `cache.invalidate(tag)` for\n * each, and invokes `onRevalidate` if configured.\n * 5. 200 with `{ ok: true, tags }`, or 502 when `onRevalidate` throws and\n * `webhook.failOnWarmError` is true (default).\n */\n\nimport { timingSafeEqual } from 'node:crypto';\nimport { createCache } from '../cache/index.js';\nimport type { CacheManager } from '../cache/manager.js';\nimport { revalidateConfigSchema } from '../types/config.js';\nimport type { RevalidateConfig, RevalidateConfigInput } from '../types/config.js';\nimport { buildWebhookEvent } from './event.js';\nimport { mapModelToTags } from './tags.js';\nimport type { WebhookPayload, WebhookRequest, WebhookResponse } from './types.js';\n\n/** Async function shape returned by `createWebhookHandler`. */\nexport type WebhookHandler = (req: WebhookRequest, res: WebhookResponse) => Promise<void>;\n\n/**\n * Build a webhook handler bound to a validated config and the cache\n * manager derived from it.\n *\n * Accepts either a `RevalidateConfigInput` (raw user object — gets validated)\n * or a pair of pre-built dependencies, so power users can share a single\n * `CacheManager` across handlers and content fetchers.\n */\nexport function createWebhookHandler(input: RevalidateConfigInput): WebhookHandler;\nexport function createWebhookHandler(deps: {\n config: RevalidateConfig;\n cache: CacheManager;\n}): WebhookHandler;\nexport function createWebhookHandler(\n input: RevalidateConfigInput | { config: RevalidateConfig; cache: CacheManager }\n): WebhookHandler {\n const { config, cache } = resolveDeps(input);\n\n return async function handle(req, res) {\n if (req.method && req.method.toUpperCase() !== 'POST') {\n res.status(405);\n res.json({ ok: false, error: 'Method not allowed' });\n return;\n }\n\n const configuredSecret = config.webhook?.secret;\n if (configuredSecret) {\n const provided = readSecret(req);\n if (!provided || !constantTimeEqual(provided, configuredSecret)) {\n res.status(401);\n res.json({ ok: false, error: 'Invalid secret' });\n return;\n }\n }\n\n let payload: WebhookPayload;\n try {\n payload = await readPayload(req);\n } catch (err) {\n res.status(400);\n res.json({\n ok: false,\n error: err instanceof Error ? err.message : 'Bad payload',\n });\n return;\n }\n\n const tags = mapModelToTags(payload, config.webhook?.tagMap);\n for (const tag of tags) {\n await cache.invalidate(tag);\n }\n\n const event = buildWebhookEvent(payload, tags);\n const failOnWarmError = config.webhook?.failOnWarmError ?? true;\n\n if (config.webhook?.onRevalidate) {\n try {\n await config.webhook.onRevalidate(event);\n } catch (err) {\n console.error('[strapi-revalidate] onRevalidate hook threw:', err);\n if (failOnWarmError) {\n res.status(502);\n res.json({\n ok: false,\n invalidated: true,\n error: 'Cache warm failed',\n message: err instanceof Error ? err.message : 'Unknown warm error',\n tags,\n });\n return;\n }\n }\n }\n\n res.status(200);\n res.json({ ok: true, tags });\n };\n}\n\nfunction resolveDeps(\n input: RevalidateConfigInput | { config: RevalidateConfig; cache: CacheManager }\n): { config: RevalidateConfig; cache: CacheManager } {\n if (isPreBuilt(input)) return input;\n const config = revalidateConfigSchema.parse(input);\n const cache = createCache(config);\n return { config, cache };\n}\n\nfunction isPreBuilt(\n value: unknown\n): value is { config: RevalidateConfig; cache: CacheManager } {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'config' in value &&\n 'cache' in value &&\n typeof (value as { cache: unknown }).cache === 'object'\n );\n}\n\nfunction readSecret(req: WebhookRequest): string | null {\n // Headers can arrive as Fetch-style Headers (`.get()`) or as a plain map.\n const direct = headerValue(req, 'authorization');\n if (direct) {\n const match = /^Bearer\\s+(.+)$/i.exec(direct);\n if (match) return match[1].trim();\n }\n\n return (\n headerValue(req, 'x-strapi-signature') ??\n headerValue(req, 'strapi-webhook-secret') ??\n headerValue(req, 'x-webhook-secret') ??\n null\n );\n}\n\nfunction headerValue(req: WebhookRequest, name: string): string | null {\n const headers = req.headers;\n if (typeof headers.get === 'function') {\n return headers.get(name);\n }\n const lower = name.toLowerCase();\n const raw = (headers as Record<string, unknown>)[lower] ?? (headers as Record<string, unknown>)[name];\n if (typeof raw === 'string') return raw;\n if (Array.isArray(raw) && typeof raw[0] === 'string') return raw[0];\n return null;\n}\n\nasync function readPayload(req: WebhookRequest): Promise<WebhookPayload> {\n let raw: unknown;\n if (typeof req.json === 'function') {\n raw = await req.json();\n } else if (typeof req.text === 'function') {\n const text = await req.text();\n raw = text.length > 0 ? JSON.parse(text) : {};\n } else {\n throw new Error('Request object exposes neither json() nor text()');\n }\n\n if (!isWebhookPayload(raw)) {\n throw new Error('Payload is not a valid Strapi v5 webhook event');\n }\n return raw;\n}\n\nfunction isWebhookPayload(value: unknown): value is WebhookPayload {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.event !== 'string' || typeof v.model !== 'string' || typeof v.uid !== 'string') {\n return false;\n }\n if (typeof v.entry !== 'object' || v.entry === null) return false;\n const entry = v.entry as Record<string, unknown>;\n return typeof entry.documentId === 'string' || typeof entry.id === 'number';\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n // timingSafeEqual requires equal-length buffers; pad both to the longer length so the\n // length check itself is constant-time-safe (different lengths still return false).\n const bufA = Buffer.from(a, 'utf-8');\n const bufB = Buffer.from(b, 'utf-8');\n if (bufA.length !== bufB.length) return false;\n return timingSafeEqual(bufA, bufB);\n}\n"]}
|
package/dist/webhook/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { createWebhookHandler } from './handler.js';
|
|
6
6
|
export type { WebhookHandler } from './handler.js';
|
|
7
|
+
export { buildWebhookEvent, resolveEntryId } from './event.js';
|
|
7
8
|
export { mapModelToTags } from './tags.js';
|
|
8
9
|
export type { WebhookEvent, WebhookEventName, WebhookPayload, WebhookRequest, WebhookResponse, } from './types.js';
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webhook/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,eAAe,GAChB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webhook/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,eAAe,GAChB,MAAM,YAAY,CAAC"}
|
package/dist/webhook/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webhook/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC","sourcesContent":["/**\n * Public webhook surface — re-exports for the `strapi-revalidate/webhook`\n * subpath.\n */\n\nexport { createWebhookHandler } from './handler.js';\nexport type { WebhookHandler } from './handler.js';\nexport { mapModelToTags } from './tags.js';\nexport type {\n WebhookEvent,\n WebhookEventName,\n WebhookPayload,\n WebhookRequest,\n WebhookResponse,\n} from './types.js';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webhook/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC","sourcesContent":["/**\n * Public webhook surface — re-exports for the `strapi-revalidate/webhook`\n * subpath.\n */\n\nexport { createWebhookHandler } from './handler.js';\nexport type { WebhookHandler } from './handler.js';\nexport { buildWebhookEvent, resolveEntryId } from './event.js';\nexport { mapModelToTags } from './tags.js';\nexport type {\n WebhookEvent,\n WebhookEventName,\n WebhookPayload,\n WebhookRequest,\n WebhookResponse,\n} from './types.js';\n"]}
|
package/dist/webhook/tags.d.ts
CHANGED
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
* - `api::author.author` → `["authors"]` (+ `author:<slug>` if entry has slug)
|
|
7
7
|
* - any other `api::<name>.<name>` → `[<name>]` (+ `<name>:<slug>` if present)
|
|
8
8
|
*
|
|
9
|
+
* Slug-derived tags include the current slug and optional prior slugs when the
|
|
10
|
+
* webhook entry carries `previousSlug`, `oldSlug`, or `previous_slug` (e.g. from
|
|
11
|
+
* a Strapi lifecycle that records the value before a rename).
|
|
12
|
+
*
|
|
9
13
|
* Override per-UID via `config.webhook.tagMap` — passing `["foo", "bar"]`
|
|
10
|
-
* replaces the default tag list for that UID (
|
|
14
|
+
* replaces the default tag list for that UID (slug-derived tags are still
|
|
11
15
|
* appended automatically when present).
|
|
12
16
|
*/
|
|
13
17
|
import type { WebhookPayload } from './types.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../../src/webhook/tags.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../../src/webhook/tags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAWjD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,cAAc,EACvB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GACnC,MAAM,EAAE,CAIV"}
|
package/dist/webhook/tags.js
CHANGED
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
* - `api::author.author` → `["authors"]` (+ `author:<slug>` if entry has slug)
|
|
7
7
|
* - any other `api::<name>.<name>` → `[<name>]` (+ `<name>:<slug>` if present)
|
|
8
8
|
*
|
|
9
|
+
* Slug-derived tags include the current slug and optional prior slugs when the
|
|
10
|
+
* webhook entry carries `previousSlug`, `oldSlug`, or `previous_slug` (e.g. from
|
|
11
|
+
* a Strapi lifecycle that records the value before a rename).
|
|
12
|
+
*
|
|
9
13
|
* Override per-UID via `config.webhook.tagMap` — passing `["foo", "bar"]`
|
|
10
|
-
* replaces the default tag list for that UID (
|
|
14
|
+
* replaces the default tag list for that UID (slug-derived tags are still
|
|
11
15
|
* appended automatically when present).
|
|
12
16
|
*/
|
|
13
17
|
/** Built-in UID → tag list. */
|
|
@@ -15,6 +19,8 @@ const DEFAULT_TAG_MAP = {
|
|
|
15
19
|
'api::article.article': ['articles'],
|
|
16
20
|
'api::author.author': ['authors'],
|
|
17
21
|
};
|
|
22
|
+
/** Optional entry fields that may carry a slug from before a rename. */
|
|
23
|
+
const PREVIOUS_SLUG_KEYS = ['previousSlug', 'oldSlug', 'previous_slug'];
|
|
18
24
|
/**
|
|
19
25
|
* Resolve cache tags for a webhook payload.
|
|
20
26
|
*
|
|
@@ -24,9 +30,22 @@ const DEFAULT_TAG_MAP = {
|
|
|
24
30
|
*/
|
|
25
31
|
export function mapModelToTags(payload, overrides) {
|
|
26
32
|
const base = overrides?.[payload.uid] ?? DEFAULT_TAG_MAP[payload.uid] ?? [extractModel(payload)];
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
const slugTags = collectSlugTags(payload);
|
|
34
|
+
return Array.from(new Set([...base, ...slugTags]));
|
|
35
|
+
}
|
|
36
|
+
function collectSlugTags(payload) {
|
|
37
|
+
const slugs = new Set();
|
|
38
|
+
const entry = payload.entry;
|
|
39
|
+
if (typeof entry.slug === 'string' && entry.slug.length > 0) {
|
|
40
|
+
slugs.add(entry.slug);
|
|
41
|
+
}
|
|
42
|
+
for (const key of PREVIOUS_SLUG_KEYS) {
|
|
43
|
+
const value = entry[key];
|
|
44
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
45
|
+
slugs.add(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return Array.from(slugs).map((slug) => `${payload.model}:${slug}`);
|
|
30
49
|
}
|
|
31
50
|
/**
|
|
32
51
|
* Extract the model name from a UID. `api::article.article` → `article`.
|
package/dist/webhook/tags.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tags.js","sourceRoot":"","sources":["../../src/webhook/tags.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"tags.js","sourceRoot":"","sources":["../../src/webhook/tags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,+BAA+B;AAC/B,MAAM,eAAe,GAA6B;IAChD,sBAAsB,EAAE,CAAC,UAAU,CAAC;IACpC,oBAAoB,EAAE,CAAC,SAAS,CAAC;CAClC,CAAC;AAEF,wEAAwE;AACxE,MAAM,kBAAkB,GAAG,CAAC,cAAc,EAAE,SAAS,EAAE,eAAe,CAAU,CAAC;AAEjF;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAuB,EACvB,SAAoC;IAEpC,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IACjG,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IAC1C,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;AACrD,CAAC;AAED,SAAS,eAAe,CAAC,OAAuB;IAC9C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAE5B,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5D,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,kBAAkB,EAAE,CAAC;QACrC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClD,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC,CAAC;AACrE,CAAC;AAED;;;GAGG;AACH,SAAS,YAAY,CAAC,OAAuB;IAC3C,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAClD,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC;AACrC,CAAC","sourcesContent":["/**\n * Strapi UID → cache tag mapping.\n *\n * Default rules:\n * - `api::article.article` → `[\"articles\"]` (+ `article:<slug>` if entry has slug)\n * - `api::author.author` → `[\"authors\"]` (+ `author:<slug>` if entry has slug)\n * - any other `api::<name>.<name>` → `[<name>]` (+ `<name>:<slug>` if present)\n *\n * Slug-derived tags include the current slug and optional prior slugs when the\n * webhook entry carries `previousSlug`, `oldSlug`, or `previous_slug` (e.g. from\n * a Strapi lifecycle that records the value before a rename).\n *\n * Override per-UID via `config.webhook.tagMap` — passing `[\"foo\", \"bar\"]`\n * replaces the default tag list for that UID (slug-derived tags are still\n * appended automatically when present).\n */\n\nimport type { WebhookPayload } from './types.js';\n\n/** Built-in UID → tag list. */\nconst DEFAULT_TAG_MAP: Record<string, string[]> = {\n 'api::article.article': ['articles'],\n 'api::author.author': ['authors'],\n};\n\n/** Optional entry fields that may carry a slug from before a rename. */\nconst PREVIOUS_SLUG_KEYS = ['previousSlug', 'oldSlug', 'previous_slug'] as const;\n\n/**\n * Resolve cache tags for a webhook payload.\n *\n * @param payload - Raw Strapi v5 webhook payload.\n * @param overrides - Optional UID → tag list overrides from config.\n * @returns Deduplicated list of tags to invalidate.\n */\nexport function mapModelToTags(\n payload: WebhookPayload,\n overrides?: Record<string, string[]>\n): string[] {\n const base = overrides?.[payload.uid] ?? DEFAULT_TAG_MAP[payload.uid] ?? [extractModel(payload)];\n const slugTags = collectSlugTags(payload);\n return Array.from(new Set([...base, ...slugTags]));\n}\n\nfunction collectSlugTags(payload: WebhookPayload): string[] {\n const slugs = new Set<string>();\n const entry = payload.entry;\n\n if (typeof entry.slug === 'string' && entry.slug.length > 0) {\n slugs.add(entry.slug);\n }\n\n for (const key of PREVIOUS_SLUG_KEYS) {\n const value = entry[key];\n if (typeof value === 'string' && value.length > 0) {\n slugs.add(value);\n }\n }\n\n return Array.from(slugs).map((slug) => `${payload.model}:${slug}`);\n}\n\n/**\n * Extract the model name from a UID. `api::article.article` → `article`.\n * Falls back to `payload.model` if the UID is in an unexpected shape.\n */\nfunction extractModel(payload: WebhookPayload): string {\n const match = /^api::([^.]+)\\./.exec(payload.uid);\n return match?.[1] ?? payload.model;\n}\n"]}
|
package/dist/webhook/types.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/webhook/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,+CAA+C;AAC/C,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,cAAc,GACd,cAAc,GACd,eAAe,GACf,iBAAiB,GACjB,cAAc,GACd,cAAc,GACd,cAAc,CAAC;AAEnB,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,gBAAgB,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/webhook/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,+CAA+C;AAC/C,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,cAAc,GACd,cAAc,GACd,eAAe,GACf,iBAAiB,GACjB,cAAc,GACd,cAAc,GACd,cAAc,CAAC;AAEnB,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,gBAAgB,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE;QACL,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH;AAED,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,gBAAgB,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE;QACP,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;QACtC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;IACF,IAAI,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAClC,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/webhook/types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * Strapi v5 webhook payload types and the normalized event surfaced to\n * downstream handlers.\n */\n\n/** All webhook event names Strapi v5 emits. */\nexport type WebhookEventName =\n | 'entry.create'\n | 'entry.update'\n | 'entry.delete'\n | 'entry.publish'\n | 'entry.unpublish'\n | 'media.create'\n | 'media.update'\n | 'media.delete';\n\n/** Raw payload Strapi v5 POSTs to the webhook URL. */\nexport interface WebhookPayload {\n event: WebhookEventName;\n createdAt: string;\n /** Singular model name, e.g. `\"article\"`. */\n model: string;\n /** Strapi UID, e.g. `\"api::article.article\"`. */\n uid: string;\n entry: {\n id
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/webhook/types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * Strapi v5 webhook payload types and the normalized event surfaced to\n * downstream handlers.\n */\n\n/** All webhook event names Strapi v5 emits. */\nexport type WebhookEventName =\n | 'entry.create'\n | 'entry.update'\n | 'entry.delete'\n | 'entry.publish'\n | 'entry.unpublish'\n | 'media.create'\n | 'media.update'\n | 'media.delete';\n\n/** Raw payload Strapi v5 POSTs to the webhook URL. */\nexport interface WebhookPayload {\n event: WebhookEventName;\n createdAt: string;\n /** Singular model name, e.g. `\"article\"`. */\n model: string;\n /** Strapi UID, e.g. `\"api::article.article\"`. */\n uid: string;\n entry: {\n id?: number;\n documentId?: string;\n slug?: string;\n [key: string]: unknown;\n };\n}\n\n/** Normalized event passed to `onRevalidate` after tag resolution. */\nexport interface WebhookEvent {\n event: WebhookEventName;\n model: string;\n uid: string;\n entryId: string;\n slug?: string;\n /** Cache tags resolved from `uid` (and `slug`, if present). */\n tags: string[];\n}\n\n/**\n * Minimal request interface the webhook handler accepts.\n *\n * Designed so an Astro `APIContext.request`, a Next.js `Request`, a\n * SvelteKit `RequestEvent.request`, an Express `req`, and a Node\n * `IncomingMessage` can all satisfy it (after light adapter code in the\n * user's route handler).\n */\nexport interface WebhookRequest {\n method?: string;\n headers: {\n get?: (name: string) => string | null;\n [key: string]: unknown;\n };\n text?: () => Promise<string>;\n json?: () => Promise<unknown>;\n}\n\n/**\n * Minimal response interface the webhook handler writes to.\n *\n * The handler always calls `status(code)` first and then `json(body)`. Both\n * calls are independent — `status()`'s return value is ignored — so adapters\n * in framework routes can be a few lines of capture-and-forward.\n */\nexport interface WebhookResponse {\n status: (code: number) => unknown;\n json: (body: unknown) => void | Promise<void>;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datum-cloud/strapi-revalidate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Framework-agnostic runtime cache invalidation for Strapi v5 in SSR frontends. Receives Strapi publish webhooks, clears affected cache tags, next request serves fresh data.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"strapi",
|