payload-smart-cache 1.2.3 → 1.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/README.md CHANGED
@@ -78,12 +78,67 @@ const getPosts = createRequestHandler(
78
78
 
79
79
  ### Options
80
80
 
81
- | Option | Type | Default | Description |
82
- | --------------------- | ----------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------- |
83
- | `collections` | `CollectionSlug[]` | `[]` | Collections to track changes for. Referenced collections are auto-tracked. |
84
- | `globals` | `GlobalSlug[]` | `[]` | Globals to track changes for. Referenced collections are auto-tracked. |
85
- | `disableAutoTracking` | `boolean` | `false` | Disable automatic tracking of collections referenced via relationship/upload fields. |
86
- | `onInvalidate` | `(change) => void \| Promise<void>` | — | Called when cache invalidation fires for a registered collection (`{ type: 'collection', slug, docID }`) or global (`{ type: 'global', slug }`). |
81
+ | Option | Type | Default | Description |
82
+ | --------------------- | ----------------------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------- |
83
+ | `collections` | `CollectionSlug[]` | `[]` | Collections to track changes for. Referenced collections are auto-tracked. |
84
+ | `globals` | `GlobalSlug[]` | `[]` | Globals to track changes for. Referenced collections are auto-tracked. |
85
+ | `disableAutoTracking` | `boolean` | `false` | Disable automatic tracking of collections referenced via relationship/upload fields. |
86
+ | `onInvalidate` | `(change) => void \| Promise<void>` | — | Called when cache invalidation fires for a registered collection (`{ type: 'collection', slug, docID }`) or global (`{ type: 'global', slug }`). |
87
+ | `tenantField` | `string` | `undefined` | Name of the tenant relationship field. When set, cache invalidation is scoped per-tenant. Collections without this field use global invalidation. |
88
+
89
+ ### Multi-Tenant Support
90
+
91
+ For multi-tenant Payload applications using `@payloadcms/plugin-multi-tenant` (or a custom tenant field), set `tenantField` to scope cache invalidation per tenant. When a tenant's document changes, only that tenant's cached data is revalidated — other tenants' caches remain warm.
92
+
93
+ ```ts
94
+ smartCachePlugin({
95
+ collections: ["posts", "media", "members", "events"],
96
+ tenantField: "tenant", // matches your multi-tenant plugin field name
97
+ })
98
+ ```
99
+
100
+ Collections without the tenant field (e.g., shared content) are automatically detected and use global invalidation as before.
101
+
102
+ #### Tenant-scoped data fetching
103
+
104
+ **Next.js 15+ (unstable_cache)**
105
+
106
+ ```ts
107
+ import { createTenantRequestHandler } from "payload-smart-cache";
108
+
109
+ const getPosts = createTenantRequestHandler(
110
+ async (tenantId: string, slug: string) => {
111
+ const payload = await getPayload({ config });
112
+ return payload.find({
113
+ collection: "posts",
114
+ where: { slug: { equals: slug }, tenant: { equals: tenantId } },
115
+ });
116
+ },
117
+ (tenantId) => ["posts", `posts:${tenantId}`],
118
+ );
119
+
120
+ // Usage
121
+ const posts = await getPosts(tenantId, "my-post");
122
+ ```
123
+
124
+ **Next.js 16+ ("use cache" directive)**
125
+
126
+ ```ts
127
+ import { tenantCacheTag } from "payload-smart-cache/cache";
128
+
129
+ async function getPosts(tenantId: string) {
130
+ "use cache";
131
+ tenantCacheTag("posts", tenantId);
132
+
133
+ const payload = await getPayload({ config });
134
+ return payload.find({
135
+ collection: "posts",
136
+ where: { tenant: { equals: tenantId } },
137
+ });
138
+ }
139
+ ```
140
+
141
+ `tenantCacheTag` requires `cacheComponents: true` in your `next.config`.
87
142
 
88
143
  ## Contributing
89
144
 
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Applies tenant-scoped cache tags for use with Next.js 16+ `"use cache"` directive.
3
+ * For each slug, registers both the base tag and a tenant-scoped tag (`slug:tenantId`).
4
+ *
5
+ * @param slugs - Collection/global slug(s) to tag.
6
+ * @param tenantId - The tenant ID to scope the cache tags to.
7
+ */
8
+ export declare function tenantCacheTag(slugs: string | string[], tenantId: string): void;
9
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/exports/cache.ts"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,QAAQ,EAAE,MAAM,GACf,IAAI,CAIN"}
@@ -0,0 +1,24 @@
1
+ // cacheTag is available in Next.js 16+ (stable) or 15.x with experimental.dynamicIO.
2
+ // Since the project supports next >= 15.5.9, this import may not resolve in all versions.
3
+ // This module should only be used by consumers on Next.js 16+ with cacheComponents enabled.
4
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
5
+ // @ts-expect-error — cacheTag is not exported in Next.js 15 types
6
+ import { cacheTag } from 'next/cache';
7
+ /**
8
+ * Applies tenant-scoped cache tags for use with Next.js 16+ `"use cache"` directive.
9
+ * For each slug, registers both the base tag and a tenant-scoped tag (`slug:tenantId`).
10
+ *
11
+ * @param slugs - Collection/global slug(s) to tag.
12
+ * @param tenantId - The tenant ID to scope the cache tags to.
13
+ */ export function tenantCacheTag(slugs, tenantId) {
14
+ const slugArray = Array.isArray(slugs) ? slugs : [
15
+ slugs
16
+ ];
17
+ const tags = slugArray.flatMap((slug)=>[
18
+ slug,
19
+ `${slug}:${tenantId}`
20
+ ]);
21
+ cacheTag(...tags);
22
+ }
23
+
24
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/exports/cache.ts"],"sourcesContent":["// cacheTag is available in Next.js 16+ (stable) or 15.x with experimental.dynamicIO.\n// Since the project supports next >= 15.5.9, this import may not resolve in all versions.\n// This module should only be used by consumers on Next.js 16+ with cacheComponents enabled.\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-expect-error — cacheTag is not exported in Next.js 15 types\nimport { cacheTag } from 'next/cache';\n\n/**\n * Applies tenant-scoped cache tags for use with Next.js 16+ `\"use cache\"` directive.\n * For each slug, registers both the base tag and a tenant-scoped tag (`slug:tenantId`).\n *\n * @param slugs - Collection/global slug(s) to tag.\n * @param tenantId - The tenant ID to scope the cache tags to.\n */\nexport function tenantCacheTag(\n slugs: string | string[],\n tenantId: string,\n): void {\n const slugArray = Array.isArray(slugs) ? slugs : [slugs];\n const tags = slugArray.flatMap((slug) => [slug, `${slug}:${tenantId}`]);\n cacheTag(...tags);\n}\n"],"names":["cacheTag","tenantCacheTag","slugs","tenantId","slugArray","Array","isArray","tags","flatMap","slug"],"mappings":"AAAA,qFAAqF;AACrF,0FAA0F;AAC1F,4FAA4F;AAC5F,6DAA6D;AAC7D,kEAAkE;AAClE,SAASA,QAAQ,QAAQ,aAAa;AAEtC;;;;;;CAMC,GACD,OAAO,SAASC,eACdC,KAAwB,EACxBC,QAAgB;IAEhB,MAAMC,YAAYC,MAAMC,OAAO,CAACJ,SAASA,QAAQ;QAACA;KAAM;IACxD,MAAMK,OAAOH,UAAUI,OAAO,CAAC,CAACC,OAAS;YAACA;YAAM,GAAGA,KAAK,CAAC,EAAEN,UAAU;SAAC;IACtEH,YAAYO;AACd"}
@@ -0,0 +1,16 @@
1
+ export interface TenantRequestHandlerOptions {
2
+ /** Time-based revalidation in seconds, or `false` to rely solely on tag-based revalidation. */
3
+ revalidate?: number | false;
4
+ }
5
+ /**
6
+ * Wraps a data-fetching function with tenant-scoped Next.js `unstable_cache`.
7
+ * The first argument to the handler is always the tenant ID, used to:
8
+ * - Generate tenant-scoped cache tags via `tagsFn`
9
+ * - Create a separate `unstable_cache` wrapper per tenant (memoized)
10
+ *
11
+ * @param handler - The async function to cache. First arg must be the tenant ID.
12
+ * @param tagsFn - Function that receives the tenant ID and returns cache tags.
13
+ * @param options - Additional cache options.
14
+ */
15
+ export declare const createTenantRequestHandler: <Data, Inputs extends [string, ...unknown[]]>(handler: (...inputs: Inputs) => Promise<Data>, tagsFn: (tenantId: string) => string[], options?: TenantRequestHandlerOptions) => ((...inputs: Inputs) => Promise<Data>);
16
+ //# sourceMappingURL=create-tenant-request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-tenant-request.d.ts","sourceRoot":"","sources":["../../src/exports/create-tenant-request.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,2BAA2B;IAC1C,+FAA+F;IAC/F,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC7B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,GACrC,IAAI,EACJ,MAAM,SAAS,CAAC,MAAM,EAAE,GAAG,OAAO,EAAE,CAAC,WAE5B,CAAC,GAAG,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,UACrC,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,EAAE,YAC5B,2BAA2B,KACpC,CAAC,CAAC,GAAG,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAiBvC,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { unstable_cache } from 'next/cache';
2
+ /**
3
+ * Wraps a data-fetching function with tenant-scoped Next.js `unstable_cache`.
4
+ * The first argument to the handler is always the tenant ID, used to:
5
+ * - Generate tenant-scoped cache tags via `tagsFn`
6
+ * - Create a separate `unstable_cache` wrapper per tenant (memoized)
7
+ *
8
+ * @param handler - The async function to cache. First arg must be the tenant ID.
9
+ * @param tagsFn - Function that receives the tenant ID and returns cache tags.
10
+ * @param options - Additional cache options.
11
+ */ export const createTenantRequestHandler = (handler, tagsFn, options)=>{
12
+ const cache = new Map();
13
+ return (...inputs)=>{
14
+ const tenantId = inputs[0];
15
+ let cached = cache.get(tenantId);
16
+ if (!cached) {
17
+ cached = unstable_cache(handler, [
18
+ tenantId
19
+ ], {
20
+ tags: tagsFn(tenantId),
21
+ revalidate: options?.revalidate ?? false
22
+ });
23
+ cache.set(tenantId, cached);
24
+ }
25
+ return cached(...inputs);
26
+ };
27
+ };
28
+
29
+ //# sourceMappingURL=create-tenant-request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/exports/create-tenant-request.ts"],"sourcesContent":["import { unstable_cache } from 'next/cache';\n\nexport interface TenantRequestHandlerOptions {\n /** Time-based revalidation in seconds, or `false` to rely solely on tag-based revalidation. */\n revalidate?: number | false;\n}\n\n/**\n * Wraps a data-fetching function with tenant-scoped Next.js `unstable_cache`.\n * The first argument to the handler is always the tenant ID, used to:\n * - Generate tenant-scoped cache tags via `tagsFn`\n * - Create a separate `unstable_cache` wrapper per tenant (memoized)\n *\n * @param handler - The async function to cache. First arg must be the tenant ID.\n * @param tagsFn - Function that receives the tenant ID and returns cache tags.\n * @param options - Additional cache options.\n */\nexport const createTenantRequestHandler = <\n Data,\n Inputs extends [string, ...unknown[]],\n>(\n handler: (...inputs: Inputs) => Promise<Data>,\n tagsFn: (tenantId: string) => string[],\n options?: TenantRequestHandlerOptions,\n): ((...inputs: Inputs) => Promise<Data>) => {\n const cache = new Map<string, (...inputs: Inputs) => Promise<Data>>();\n\n return (...inputs: Inputs): Promise<Data> => {\n const tenantId = inputs[0];\n\n let cached = cache.get(tenantId);\n if (!cached) {\n cached = unstable_cache(handler, [tenantId], {\n tags: tagsFn(tenantId),\n revalidate: options?.revalidate ?? false,\n });\n cache.set(tenantId, cached);\n }\n\n return cached(...inputs);\n };\n};\n"],"names":["unstable_cache","createTenantRequestHandler","handler","tagsFn","options","cache","Map","inputs","tenantId","cached","get","tags","revalidate","set"],"mappings":"AAAA,SAASA,cAAc,QAAQ,aAAa;AAO5C;;;;;;;;;CASC,GACD,OAAO,MAAMC,6BAA6B,CAIxCC,SACAC,QACAC;IAEA,MAAMC,QAAQ,IAAIC;IAElB,OAAO,CAAC,GAAGC;QACT,MAAMC,WAAWD,MAAM,CAAC,EAAE;QAE1B,IAAIE,SAASJ,MAAMK,GAAG,CAACF;QACvB,IAAI,CAACC,QAAQ;YACXA,SAAST,eAAeE,SAAS;gBAACM;aAAS,EAAE;gBAC3CG,MAAMR,OAAOK;gBACbI,YAAYR,SAASQ,cAAc;YACrC;YACAP,MAAMQ,GAAG,CAACL,UAAUC;QACtB;QAEA,OAAOA,UAAUF;IACnB;AACF,EAAE"}
package/dist/hooks.d.ts CHANGED
@@ -1,13 +1,16 @@
1
- import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, GlobalAfterChangeHook } from 'payload';
1
+ import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, CollectionSlug, GlobalAfterChangeHook } from 'payload';
2
2
  import type { DocumentInvalidationCallback, DocumentWithStatus } from './types';
3
3
  import type { EntitiesGraph } from './utils/dependency-graph';
4
- export declare function invalidateCollectionCache({ graph, invalidationCallback, }: {
4
+ interface CollectionHookConfig {
5
5
  graph: EntitiesGraph;
6
6
  invalidationCallback: DocumentInvalidationCallback | undefined;
7
- }): CollectionAfterChangeHook<DocumentWithStatus>;
8
- export declare function invalidateCollectionCacheOnDelete({ graph, invalidationCallback, }: {
9
- graph: EntitiesGraph;
7
+ tenantField?: string;
8
+ tenantScopedCollections?: Set<CollectionSlug>;
9
+ }
10
+ export declare function invalidateCollectionCache({ graph, invalidationCallback, tenantField, tenantScopedCollections, }: CollectionHookConfig): CollectionAfterChangeHook<DocumentWithStatus>;
11
+ export declare function invalidateCollectionCacheOnDelete({ graph, invalidationCallback, tenantField, tenantScopedCollections, }: CollectionHookConfig & {
10
12
  invalidationCallback: DocumentInvalidationCallback;
11
13
  }): CollectionAfterDeleteHook<DocumentWithStatus>;
12
14
  export declare function invalidateGlobalCache(invalidationCallback: DocumentInvalidationCallback): GlobalAfterChangeHook;
15
+ export {};
13
16
  //# sourceMappingURL=hooks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,yBAAyB,EACzB,yBAAyB,EAEzB,qBAAqB,EACtB,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,4BAA4B,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AA4G9D,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,oBAAoB,GACrB,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,oBAAoB,EAAE,4BAA4B,GAAG,SAAS,CAAC;CAChE,GAAG,yBAAyB,CAAC,kBAAkB,CAAC,CAgBhD;AAED,wBAAgB,iCAAiC,CAAC,EAChD,KAAK,EACL,oBAAoB,GACrB,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,oBAAoB,EAAE,4BAA4B,CAAC;CACpD,GAAG,yBAAyB,CAAC,kBAAkB,CAAC,CAWhD;AAED,wBAAgB,qBAAqB,CACnC,oBAAoB,EAAE,4BAA4B,GACjD,qBAAqB,CAWvB"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,yBAAyB,EACzB,yBAAyB,EACzB,cAAc,EACd,qBAAqB,EACtB,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,4BAA4B,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AA4K9D,UAAU,oBAAoB;IAC5B,KAAK,EAAE,aAAa,CAAC;IACrB,oBAAoB,EAAE,4BAA4B,GAAG,SAAS,CAAC;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uBAAuB,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;CAC/C;AAED,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,oBAAoB,EACpB,WAAW,EACX,uBAAmC,GACpC,EAAE,oBAAoB,GAAG,yBAAyB,CAAC,kBAAkB,CAAC,CAyBtE;AAED,wBAAgB,iCAAiC,CAAC,EAChD,KAAK,EACL,oBAAoB,EACpB,WAAW,EACX,uBAAmC,GACpC,EAAE,oBAAoB,GAAG;IACxB,oBAAoB,EAAE,4BAA4B,CAAC;CACpD,GAAG,yBAAyB,CAAC,kBAAkB,CAAC,CAoBhD;AAED,wBAAgB,qBAAqB,CACnC,oBAAoB,EAAE,4BAA4B,GACjD,qBAAqB,CAWvB"}
package/dist/hooks.js CHANGED
@@ -1,8 +1,15 @@
1
1
  import { revalidateTag } from 'next/cache';
2
- async function invalidateWithDependents(payload, { graph, invalidationCallback, collection, ids }) {
2
+ import { resolveTenantId } from './utils/resolve-tenant-id';
3
+ function buildTag(slug, tenantId, tenantScopedCollections) {
4
+ if (tenantId && tenantScopedCollections.has(slug)) {
5
+ return `${slug}:${tenantId}`;
6
+ }
7
+ return slug;
8
+ }
9
+ async function invalidateWithDependents(payload, { graph, invalidationCallback, collection, ids, tenantId, tenantConfig }) {
3
10
  const tagsToInvalidate = new Set();
4
- tagsToInvalidate.add(collection);
5
- await walkDependents(graph, payload, collection, ids, new Set());
11
+ tagsToInvalidate.add(buildTag(collection, tenantId, tenantConfig.tenantScopedCollections));
12
+ await walkDependents(graph, payload, collection, ids, new Set(), tenantId, tenantConfig);
6
13
  for (const tag of tagsToInvalidate){
7
14
  revalidateTag(tag);
8
15
  }
@@ -10,10 +17,11 @@ async function invalidateWithDependents(payload, { graph, invalidationCallback,
10
17
  await invalidationCallback?.({
11
18
  type: 'collection',
12
19
  slug: collection,
13
- docID: id
20
+ docID: id,
21
+ tenantId
14
22
  });
15
23
  }
16
- async function walkDependents(graph, payload, changedCollection, changedIds, visited) {
24
+ async function walkDependents(graph, payload, changedCollection, changedIds, visited, tenantId, tenantConfig) {
17
25
  const dependents = graph.getDependants(changedCollection);
18
26
  if (dependents.length === 0) return;
19
27
  for (const dependent of dependents){
@@ -22,28 +30,41 @@ async function invalidateWithDependents(payload, { graph, invalidationCallback,
22
30
  continue;
23
31
  }
24
32
  if (visited.has(dependent.entity.slug)) continue;
33
+ const depIsTenantScoped = tenantConfig.tenantScopedCollections.has(dependent.entity.slug);
34
+ const effectiveTenantId = depIsTenantScoped ? tenantId : undefined;
25
35
  const allAffectedItems = new Map();
26
36
  for (const field of dependent.fields){
27
- const { docs } = await payload.find({
28
- collection: dependent.entity.slug,
29
- where: field.polymorphic ? {
30
- and: [
31
- {
32
- [`${field.field}.relationTo`]: {
33
- equals: changedCollection
34
- }
35
- },
36
- {
37
- [`${field.field}.value`]: {
38
- in: changedIds
39
- }
37
+ const baseWhere = field.polymorphic ? {
38
+ and: [
39
+ {
40
+ [`${field.field}.relationTo`]: {
41
+ equals: changedCollection
42
+ }
43
+ },
44
+ {
45
+ [`${field.field}.value`]: {
46
+ in: changedIds
40
47
  }
41
- ]
42
- } : {
43
- [field.field]: {
44
- in: changedIds
45
48
  }
49
+ ]
50
+ } : {
51
+ [field.field]: {
52
+ in: changedIds
46
53
  }
54
+ };
55
+ const where = effectiveTenantId && tenantConfig.tenantField ? {
56
+ and: [
57
+ baseWhere,
58
+ {
59
+ [tenantConfig.tenantField]: {
60
+ equals: effectiveTenantId
61
+ }
62
+ }
63
+ ]
64
+ } : baseWhere;
65
+ const { docs } = await payload.find({
66
+ collection: dependent.entity.slug,
67
+ where
47
68
  });
48
69
  for (const item of docs){
49
70
  allAffectedItems.set(item.id.toString(), item);
@@ -52,44 +73,59 @@ async function invalidateWithDependents(payload, { graph, invalidationCallback,
52
73
  const affectedItems = Array.from(allAffectedItems.values());
53
74
  visited.add(dependent.entity.slug);
54
75
  if (affectedItems.length === 0) continue;
55
- tagsToInvalidate.add(dependent.entity.slug);
76
+ tagsToInvalidate.add(buildTag(dependent.entity.slug, effectiveTenantId, tenantConfig.tenantScopedCollections));
56
77
  for (const item of affectedItems){
57
78
  await invalidationCallback?.({
58
79
  type: 'collection',
59
80
  slug: dependent.entity.slug,
60
- docID: item.id.toString()
81
+ docID: item.id.toString(),
82
+ tenantId: effectiveTenantId
61
83
  });
62
84
  }
63
- await walkDependents(graph, payload, dependent.entity.slug, affectedItems.map((item)=>item.id.toString()), visited);
85
+ await walkDependents(graph, payload, dependent.entity.slug, affectedItems.map((item)=>item.id.toString()), visited, effectiveTenantId, tenantConfig);
64
86
  }
65
87
  }
66
88
  }
67
- export function invalidateCollectionCache({ graph, invalidationCallback }) {
89
+ export function invalidateCollectionCache({ graph, invalidationCallback, tenantField, tenantScopedCollections = new Set() }) {
90
+ const tenantConfig = {
91
+ tenantField,
92
+ tenantScopedCollections
93
+ };
68
94
  return async ({ req, doc, collection, previousDoc })=>{
69
95
  if (req.context.skipRevalidation) return;
70
96
  if (collection.versions?.drafts) {
71
97
  if (doc._status === 'draft' && previousDoc._status !== 'published') return;
72
98
  }
99
+ const tenantId = resolveTenantId(doc, tenantField);
73
100
  await invalidateWithDependents(req.payload, {
74
101
  graph,
75
102
  invalidationCallback,
76
103
  collection: collection.slug,
77
104
  ids: [
78
105
  doc.id.toString()
79
- ]
106
+ ],
107
+ tenantId,
108
+ tenantConfig
80
109
  });
81
110
  };
82
111
  }
83
- export function invalidateCollectionCacheOnDelete({ graph, invalidationCallback }) {
112
+ export function invalidateCollectionCacheOnDelete({ graph, invalidationCallback, tenantField, tenantScopedCollections = new Set() }) {
113
+ const tenantConfig = {
114
+ tenantField,
115
+ tenantScopedCollections
116
+ };
84
117
  return async ({ req, doc, collection })=>{
85
118
  if (req.context.skipRevalidation) return;
119
+ const tenantId = resolveTenantId(doc, tenantField);
86
120
  await invalidateWithDependents(req.payload, {
87
121
  graph,
88
122
  invalidationCallback,
89
123
  collection: collection.slug,
90
124
  ids: [
91
125
  doc.id.toString()
92
- ]
126
+ ],
127
+ tenantId,
128
+ tenantConfig
93
129
  });
94
130
  };
95
131
  }
package/dist/hooks.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hooks.ts"],"sourcesContent":["import { revalidateTag } from 'next/cache';\nimport type {\n BasePayload,\n CollectionAfterChangeHook,\n CollectionAfterDeleteHook,\n CollectionSlug,\n GlobalAfterChangeHook,\n} from 'payload';\nimport type { DocumentInvalidationCallback, DocumentWithStatus } from './types';\nimport type { EntitiesGraph } from './utils/dependency-graph';\n\nasync function invalidateWithDependents(\n payload: BasePayload,\n {\n graph,\n invalidationCallback,\n collection,\n ids,\n }: {\n graph: EntitiesGraph;\n invalidationCallback: DocumentInvalidationCallback | undefined;\n collection: CollectionSlug;\n ids: string[];\n },\n): Promise<void> {\n const tagsToInvalidate = new Set<string>();\n\n tagsToInvalidate.add(collection);\n\n await walkDependents(graph, payload, collection, ids, new Set());\n\n for (const tag of tagsToInvalidate) {\n revalidateTag(tag);\n }\n\n for (const id of ids) {\n await invalidationCallback?.({\n type: 'collection',\n slug: collection,\n docID: id,\n });\n }\n\n async function walkDependents(\n graph: EntitiesGraph,\n payload: BasePayload,\n changedCollection: CollectionSlug,\n changedIds: string[],\n visited: Set<string>,\n ): Promise<void> {\n const dependents = graph.getDependants(changedCollection);\n\n if (dependents.length === 0) return;\n\n for (const dependent of dependents) {\n if (dependent.entity.type === 'global') {\n tagsToInvalidate.add(dependent.entity.slug);\n continue;\n }\n\n if (visited.has(dependent.entity.slug)) continue;\n\n const allAffectedItems = new Map<string, { id: string | number }>();\n\n for (const field of dependent.fields) {\n const { docs } = await payload.find({\n collection: dependent.entity.slug,\n where: field.polymorphic\n ? {\n and: [\n {\n [`${field.field}.relationTo`]: {\n equals: changedCollection,\n },\n },\n { [`${field.field}.value`]: { in: changedIds } },\n ],\n }\n : {\n [field.field]: {\n in: changedIds,\n },\n },\n });\n\n for (const item of docs) {\n allAffectedItems.set(item.id.toString(), item);\n }\n }\n\n const affectedItems = Array.from(allAffectedItems.values());\n\n visited.add(dependent.entity.slug);\n\n if (affectedItems.length === 0) continue;\n\n tagsToInvalidate.add(dependent.entity.slug);\n\n for (const item of affectedItems) {\n await invalidationCallback?.({\n type: 'collection',\n slug: dependent.entity.slug,\n docID: item.id.toString(),\n });\n }\n\n await walkDependents(\n graph,\n payload,\n dependent.entity.slug,\n affectedItems.map((item) => item.id.toString()),\n visited,\n );\n }\n }\n}\n\nexport function invalidateCollectionCache({\n graph,\n invalidationCallback,\n}: {\n graph: EntitiesGraph;\n invalidationCallback: DocumentInvalidationCallback | undefined;\n}): CollectionAfterChangeHook<DocumentWithStatus> {\n return async ({ req, doc, collection, previousDoc }) => {\n if (req.context.skipRevalidation) return;\n\n if (collection.versions?.drafts) {\n if (doc._status === 'draft' && previousDoc._status !== 'published')\n return;\n }\n\n await invalidateWithDependents(req.payload, {\n graph,\n invalidationCallback,\n collection: collection.slug,\n ids: [doc.id.toString()],\n });\n };\n}\n\nexport function invalidateCollectionCacheOnDelete({\n graph,\n invalidationCallback,\n}: {\n graph: EntitiesGraph;\n invalidationCallback: DocumentInvalidationCallback;\n}): CollectionAfterDeleteHook<DocumentWithStatus> {\n return async ({ req, doc, collection }) => {\n if (req.context.skipRevalidation) return;\n\n await invalidateWithDependents(req.payload, {\n graph,\n invalidationCallback,\n collection: collection.slug,\n ids: [doc.id.toString()],\n });\n };\n}\n\nexport function invalidateGlobalCache(\n invalidationCallback: DocumentInvalidationCallback,\n): GlobalAfterChangeHook {\n return async ({ req, global, doc, previousDoc }) => {\n if (global.versions?.drafts) {\n if (doc._status === 'draft' && previousDoc._status !== 'published')\n return;\n }\n if (req.context.skipRevalidation) return;\n\n revalidateTag(global.slug);\n await invalidationCallback?.({ type: 'global', slug: global.slug });\n };\n}\n"],"names":["revalidateTag","invalidateWithDependents","payload","graph","invalidationCallback","collection","ids","tagsToInvalidate","Set","add","walkDependents","tag","id","type","slug","docID","changedCollection","changedIds","visited","dependents","getDependants","length","dependent","entity","has","allAffectedItems","Map","field","fields","docs","find","where","polymorphic","and","equals","in","item","set","toString","affectedItems","Array","from","values","map","invalidateCollectionCache","req","doc","previousDoc","context","skipRevalidation","versions","drafts","_status","invalidateCollectionCacheOnDelete","invalidateGlobalCache","global"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/hooks.ts"],"sourcesContent":["import { revalidateTag } from 'next/cache';\nimport type {\n BasePayload,\n CollectionAfterChangeHook,\n CollectionAfterDeleteHook,\n CollectionSlug,\n GlobalAfterChangeHook,\n} from 'payload';\nimport type { DocumentInvalidationCallback, DocumentWithStatus } from './types';\nimport type { EntitiesGraph } from './utils/dependency-graph';\nimport { resolveTenantId } from './utils/resolve-tenant-id';\n\ninterface TenantConfig {\n tenantField?: string;\n tenantScopedCollections: Set<CollectionSlug>;\n}\n\nfunction buildTag(\n slug: string,\n tenantId: string | undefined,\n tenantScopedCollections: Set<CollectionSlug>,\n): string {\n if (tenantId && tenantScopedCollections.has(slug as CollectionSlug)) {\n return `${slug}:${tenantId}`;\n }\n return slug;\n}\n\nasync function invalidateWithDependents(\n payload: BasePayload,\n {\n graph,\n invalidationCallback,\n collection,\n ids,\n tenantId,\n tenantConfig,\n }: {\n graph: EntitiesGraph;\n invalidationCallback: DocumentInvalidationCallback | undefined;\n collection: CollectionSlug;\n ids: string[];\n tenantId: string | undefined;\n tenantConfig: TenantConfig;\n },\n): Promise<void> {\n const tagsToInvalidate = new Set<string>();\n\n tagsToInvalidate.add(\n buildTag(collection, tenantId, tenantConfig.tenantScopedCollections),\n );\n\n await walkDependents(\n graph,\n payload,\n collection,\n ids,\n new Set(),\n tenantId,\n tenantConfig,\n );\n\n for (const tag of tagsToInvalidate) {\n revalidateTag(tag);\n }\n\n for (const id of ids) {\n await invalidationCallback?.({\n type: 'collection',\n slug: collection,\n docID: id,\n tenantId,\n });\n }\n\n async function walkDependents(\n graph: EntitiesGraph,\n payload: BasePayload,\n changedCollection: CollectionSlug,\n changedIds: string[],\n visited: Set<string>,\n tenantId: string | undefined,\n tenantConfig: TenantConfig,\n ): Promise<void> {\n const dependents = graph.getDependants(changedCollection);\n\n if (dependents.length === 0) return;\n\n for (const dependent of dependents) {\n if (dependent.entity.type === 'global') {\n tagsToInvalidate.add(dependent.entity.slug);\n continue;\n }\n\n if (visited.has(dependent.entity.slug)) continue;\n\n const depIsTenantScoped = tenantConfig.tenantScopedCollections.has(\n dependent.entity.slug as CollectionSlug,\n );\n const effectiveTenantId = depIsTenantScoped ? tenantId : undefined;\n\n const allAffectedItems = new Map<string, { id: string | number }>();\n\n for (const field of dependent.fields) {\n const baseWhere = field.polymorphic\n ? {\n and: [\n {\n [`${field.field}.relationTo`]: {\n equals: changedCollection,\n },\n },\n { [`${field.field}.value`]: { in: changedIds } },\n ],\n }\n : {\n [field.field]: {\n in: changedIds,\n },\n };\n\n const where =\n effectiveTenantId && tenantConfig.tenantField\n ? {\n and: [\n baseWhere,\n {\n [tenantConfig.tenantField]: {\n equals: effectiveTenantId,\n },\n },\n ],\n }\n : baseWhere;\n\n const { docs } = await payload.find({\n collection: dependent.entity.slug,\n where,\n });\n\n for (const item of docs) {\n allAffectedItems.set(item.id.toString(), item);\n }\n }\n\n const affectedItems = Array.from(allAffectedItems.values());\n\n visited.add(dependent.entity.slug);\n\n if (affectedItems.length === 0) continue;\n\n tagsToInvalidate.add(\n buildTag(\n dependent.entity.slug,\n effectiveTenantId,\n tenantConfig.tenantScopedCollections,\n ),\n );\n\n for (const item of affectedItems) {\n await invalidationCallback?.({\n type: 'collection',\n slug: dependent.entity.slug,\n docID: item.id.toString(),\n tenantId: effectiveTenantId,\n });\n }\n\n await walkDependents(\n graph,\n payload,\n dependent.entity.slug,\n affectedItems.map((item) => item.id.toString()),\n visited,\n effectiveTenantId,\n tenantConfig,\n );\n }\n }\n}\n\ninterface CollectionHookConfig {\n graph: EntitiesGraph;\n invalidationCallback: DocumentInvalidationCallback | undefined;\n tenantField?: string;\n tenantScopedCollections?: Set<CollectionSlug>;\n}\n\nexport function invalidateCollectionCache({\n graph,\n invalidationCallback,\n tenantField,\n tenantScopedCollections = new Set(),\n}: CollectionHookConfig): CollectionAfterChangeHook<DocumentWithStatus> {\n const tenantConfig: TenantConfig = { tenantField, tenantScopedCollections };\n\n return async ({ req, doc, collection, previousDoc }) => {\n if (req.context.skipRevalidation) return;\n\n if (collection.versions?.drafts) {\n if (doc._status === 'draft' && previousDoc._status !== 'published')\n return;\n }\n\n const tenantId = resolveTenantId(\n doc as Record<string, unknown>,\n tenantField,\n );\n\n await invalidateWithDependents(req.payload, {\n graph,\n invalidationCallback,\n collection: collection.slug,\n ids: [doc.id.toString()],\n tenantId,\n tenantConfig,\n });\n };\n}\n\nexport function invalidateCollectionCacheOnDelete({\n graph,\n invalidationCallback,\n tenantField,\n tenantScopedCollections = new Set(),\n}: CollectionHookConfig & {\n invalidationCallback: DocumentInvalidationCallback;\n}): CollectionAfterDeleteHook<DocumentWithStatus> {\n const tenantConfig: TenantConfig = { tenantField, tenantScopedCollections };\n\n return async ({ req, doc, collection }) => {\n if (req.context.skipRevalidation) return;\n\n const tenantId = resolveTenantId(\n doc as Record<string, unknown>,\n tenantField,\n );\n\n await invalidateWithDependents(req.payload, {\n graph,\n invalidationCallback,\n collection: collection.slug,\n ids: [doc.id.toString()],\n tenantId,\n tenantConfig,\n });\n };\n}\n\nexport function invalidateGlobalCache(\n invalidationCallback: DocumentInvalidationCallback,\n): GlobalAfterChangeHook {\n return async ({ req, global, doc, previousDoc }) => {\n if (global.versions?.drafts) {\n if (doc._status === 'draft' && previousDoc._status !== 'published')\n return;\n }\n if (req.context.skipRevalidation) return;\n\n revalidateTag(global.slug);\n await invalidationCallback?.({ type: 'global', slug: global.slug });\n };\n}\n"],"names":["revalidateTag","resolveTenantId","buildTag","slug","tenantId","tenantScopedCollections","has","invalidateWithDependents","payload","graph","invalidationCallback","collection","ids","tenantConfig","tagsToInvalidate","Set","add","walkDependents","tag","id","type","docID","changedCollection","changedIds","visited","dependents","getDependants","length","dependent","entity","depIsTenantScoped","effectiveTenantId","undefined","allAffectedItems","Map","field","fields","baseWhere","polymorphic","and","equals","in","where","tenantField","docs","find","item","set","toString","affectedItems","Array","from","values","map","invalidateCollectionCache","req","doc","previousDoc","context","skipRevalidation","versions","drafts","_status","invalidateCollectionCacheOnDelete","invalidateGlobalCache","global"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { CollectionSlug, GlobalSlug, Plugin } from 'payload';
2
2
  import type { DocumentInvalidationCallback } from './types';
3
3
  export { createRequestHandler, type RequestHandlerCacheOptions, } from './exports/create-request';
4
+ export { createTenantRequestHandler, type TenantRequestHandlerOptions, } from './exports/create-tenant-request';
4
5
  export type { DocumentInvalidation as InvalidationChange, DocumentInvalidationCallback as OnInvalidate, } from './types';
5
6
  export interface SmartCachePluginConfig<C extends CollectionSlug = CollectionSlug, G extends GlobalSlug = GlobalSlug> {
6
7
  /**
@@ -25,6 +26,13 @@ export interface SmartCachePluginConfig<C extends CollectionSlug = CollectionSlu
25
26
  * Only fires for collections/globals explicitly registered in the config.
26
27
  */
27
28
  onInvalidate?: DocumentInvalidationCallback<C, G>;
29
+ /**
30
+ * Name of the tenant relationship field on your collections.
31
+ * When set, cache invalidation is scoped per-tenant.
32
+ * Collections without this field are treated as shared and use global invalidation.
33
+ * Must match the field name used by `@payloadcms/plugin-multi-tenant` or your custom tenant field.
34
+ */
35
+ tenantField?: string;
28
36
  }
29
- export declare const smartCachePlugin: <C extends CollectionSlug = string, G extends GlobalSlug = string>({ collections, globals, onInvalidate, disableAutoTracking, }: SmartCachePluginConfig<C, G>) => Plugin;
37
+ export declare const smartCachePlugin: <C extends CollectionSlug = string, G extends GlobalSlug = string>({ collections, globals, onInvalidate, disableAutoTracking, tenantField, }: SmartCachePluginConfig<C, G>) => Plugin;
30
38
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAMlE,OAAO,KAAK,EAEV,4BAA4B,EAE7B,MAAM,SAAS,CAAC;AAIjB,OAAO,EACL,oBAAoB,EACpB,KAAK,0BAA0B,GAChC,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,oBAAoB,IAAI,kBAAkB,EAC1C,4BAA4B,IAAI,YAAY,GAC7C,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,sBAAsB,CACrC,CAAC,SAAS,cAAc,GAAG,cAAc,EACzC,CAAC,SAAS,UAAU,GAAG,UAAU;IAEjC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;IACd;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;OAGG;IACH,YAAY,CAAC,EAAE,4BAA4B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CACnD;AAED,eAAO,MAAM,gBAAgB,GAEzB,CAAC,SAAS,cAAc,WACxB,CAAC,SAAS,UAAU,yEAMnB,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,MAmDjC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAMlE,OAAO,KAAK,EAEV,4BAA4B,EAE7B,MAAM,SAAS,CAAC;AAKjB,OAAO,EACL,oBAAoB,EACpB,KAAK,0BAA0B,GAChC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,0BAA0B,EAC1B,KAAK,2BAA2B,GACjC,MAAM,iCAAiC,CAAC;AACzC,YAAY,EACV,oBAAoB,IAAI,kBAAkB,EAC1C,4BAA4B,IAAI,YAAY,GAC7C,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,sBAAsB,CACrC,CAAC,SAAS,cAAc,GAAG,cAAc,EACzC,CAAC,SAAS,UAAU,GAAG,UAAU;IAEjC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;IACd;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;OAGG;IACH,YAAY,CAAC,EAAE,4BAA4B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAClD;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,gBAAgB,GAEzB,CAAC,SAAS,cAAc,WACxB,CAAC,SAAS,UAAU,sFAOnB,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,MA8DjC,CAAC"}
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { invalidateCollectionCache, invalidateCollectionCacheOnDelete, invalidateGlobalCache } from './hooks';
2
2
  import { createDependencyGraph } from './utils/dependency-graph';
3
+ import { getTenantScopedCollections } from './utils/tenant-scoped-collections';
3
4
  import { getTrackedCollections } from './utils/tracked-collections';
4
5
  export { createRequestHandler } from './exports/create-request';
5
- export const smartCachePlugin = ({ collections = [], globals = [], onInvalidate, disableAutoTracking })=>(config)=>{
6
+ export { createTenantRequestHandler } from './exports/create-tenant-request';
7
+ export const smartCachePlugin = ({ collections = [], globals = [], onInvalidate, disableAutoTracking, tenantField })=>(config)=>{
6
8
  if (collections.length + globals.length === 0) {
7
9
  console.warn('[payload-smart-cache] No collections or globals are configured to track changes for, hence this plugin will have no effect.');
8
10
  return config;
@@ -21,6 +23,7 @@ export const smartCachePlugin = ({ collections = [], globals = [], onInvalidate,
21
23
  global.hooks.afterChange.push(invalidateGlobalCache(invalidationCallback));
22
24
  }
23
25
  config.collections ??= [];
26
+ const tenantScopedCollections = getTenantScopedCollections(config.collections, tenantField);
24
27
  const collectionsToTrack = disableAutoTracking ? new Set(collections) : getTrackedCollections({
25
28
  collections,
26
29
  globals
@@ -34,12 +37,16 @@ export const smartCachePlugin = ({ collections = [], globals = [], onInvalidate,
34
37
  collection.hooks.afterChange ??= [];
35
38
  collection.hooks.afterChange.push(invalidateCollectionCache({
36
39
  graph,
37
- invalidationCallback
40
+ invalidationCallback,
41
+ tenantField,
42
+ tenantScopedCollections
38
43
  }));
39
44
  collection.hooks.afterDelete ??= [];
40
45
  collection.hooks.afterDelete.push(invalidateCollectionCacheOnDelete({
41
46
  graph,
42
- invalidationCallback
47
+ invalidationCallback,
48
+ tenantField,
49
+ tenantScopedCollections
43
50
  }));
44
51
  }
45
52
  return config;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { CollectionSlug, GlobalSlug, Plugin } from 'payload';\nimport {\n invalidateCollectionCache,\n invalidateCollectionCacheOnDelete,\n invalidateGlobalCache,\n} from './hooks';\nimport type {\n DocumentInvalidation,\n DocumentInvalidationCallback,\n ResolvedPluginOptions,\n} from './types';\nimport { createDependencyGraph } from './utils/dependency-graph';\nimport { getTrackedCollections } from './utils/tracked-collections';\n\nexport {\n createRequestHandler,\n type RequestHandlerCacheOptions,\n} from './exports/create-request';\nexport type {\n DocumentInvalidation as InvalidationChange,\n DocumentInvalidationCallback as OnInvalidate,\n} from './types';\n\nexport interface SmartCachePluginConfig<\n C extends CollectionSlug = CollectionSlug,\n G extends GlobalSlug = GlobalSlug,\n> {\n /**\n * The collections to track changes for.\n * By default, collections referenced via relationship or upload fields\n * are automatically tracked as well.\n */\n collections?: C[];\n /**\n * The globals to track changes for.\n * Collections referenced by these globals via relationship or upload\n * fields are automatically tracked as well.\n */\n globals?: G[];\n /**\n * Disable automatic tracking of collections referenced by the configured ones.\n * @default false\n */\n disableAutoTracking?: boolean;\n /**\n * Called when cache invalidation is triggered.\n * Only fires for collections/globals explicitly registered in the config.\n */\n onInvalidate?: DocumentInvalidationCallback<C, G>;\n}\n\nexport const smartCachePlugin =\n <\n C extends CollectionSlug = CollectionSlug,\n G extends GlobalSlug = GlobalSlug,\n >({\n collections = [],\n globals = [],\n onInvalidate,\n disableAutoTracking,\n }: SmartCachePluginConfig<C, G>): Plugin =>\n (config) => {\n if (collections.length + globals.length === 0) {\n console.warn(\n '[payload-smart-cache] No collections or globals are configured to track changes for, hence this plugin will have no effect.',\n );\n return config;\n }\n\n const graph = createDependencyGraph(config);\n\n const invalidationCallback = wrapInvalidationCallback({\n collections,\n globals,\n onInvalidate: onInvalidate as DocumentInvalidationCallback,\n });\n\n config.globals ??= [];\n for (const global of config.globals) {\n if (!globals.includes(global.slug as G)) continue;\n global.hooks ??= {};\n global.hooks.afterChange ??= [];\n global.hooks.afterChange.push(\n invalidateGlobalCache(invalidationCallback),\n );\n }\n\n config.collections ??= [];\n const collectionsToTrack = disableAutoTracking\n ? new Set(collections)\n : getTrackedCollections(\n { collections, globals },\n { collections: config.collections, globals: config.globals },\n );\n for (const collection of config.collections) {\n if (!collectionsToTrack.has(collection.slug as CollectionSlug)) continue;\n collection.hooks ??= {};\n collection.hooks.afterChange ??= [];\n collection.hooks.afterChange.push(\n invalidateCollectionCache({ graph, invalidationCallback }),\n );\n collection.hooks.afterDelete ??= [];\n collection.hooks.afterDelete.push(\n invalidateCollectionCacheOnDelete({\n graph,\n invalidationCallback,\n }),\n );\n }\n\n return config;\n };\n\nfunction wrapInvalidationCallback({\n collections,\n globals,\n onInvalidate,\n}: ResolvedPluginOptions<\n 'collections' | 'globals',\n 'onInvalidate'\n>): DocumentInvalidationCallback {\n if (!onInvalidate) return () => void 0;\n\n const registeredCollections = new Set(collections);\n const registeredGlobals = new Set(globals);\n\n return (change: DocumentInvalidation) => {\n if (change.type === 'collection' && !registeredCollections.has(change.slug))\n return;\n if (change.type === 'global' && !registeredGlobals.has(change.slug)) return;\n return onInvalidate(change);\n };\n}\n"],"names":["invalidateCollectionCache","invalidateCollectionCacheOnDelete","invalidateGlobalCache","createDependencyGraph","getTrackedCollections","createRequestHandler","smartCachePlugin","collections","globals","onInvalidate","disableAutoTracking","config","length","console","warn","graph","invalidationCallback","wrapInvalidationCallback","global","includes","slug","hooks","afterChange","push","collectionsToTrack","Set","collection","has","afterDelete","registeredCollections","registeredGlobals","change","type"],"mappings":"AACA,SACEA,yBAAyB,EACzBC,iCAAiC,EACjCC,qBAAqB,QAChB,UAAU;AAMjB,SAASC,qBAAqB,QAAQ,2BAA2B;AACjE,SAASC,qBAAqB,QAAQ,8BAA8B;AAEpE,SACEC,oBAAoB,QAEf,2BAA2B;AAkClC,OAAO,MAAMC,mBACX,CAGE,EACAC,cAAc,EAAE,EAChBC,UAAU,EAAE,EACZC,YAAY,EACZC,mBAAmB,EACU,GAC/B,CAACC;QACC,IAAIJ,YAAYK,MAAM,GAAGJ,QAAQI,MAAM,KAAK,GAAG;YAC7CC,QAAQC,IAAI,CACV;YAEF,OAAOH;QACT;QAEA,MAAMI,QAAQZ,sBAAsBQ;QAEpC,MAAMK,uBAAuBC,yBAAyB;YACpDV;YACAC;YACAC,cAAcA;QAChB;QAEAE,OAAOH,OAAO,KAAK,EAAE;QACrB,KAAK,MAAMU,UAAUP,OAAOH,OAAO,CAAE;YACnC,IAAI,CAACA,QAAQW,QAAQ,CAACD,OAAOE,IAAI,GAAQ;YACzCF,OAAOG,KAAK,KAAK,CAAC;YAClBH,OAAOG,KAAK,CAACC,WAAW,KAAK,EAAE;YAC/BJ,OAAOG,KAAK,CAACC,WAAW,CAACC,IAAI,CAC3BrB,sBAAsBc;QAE1B;QAEAL,OAAOJ,WAAW,KAAK,EAAE;QACzB,MAAMiB,qBAAqBd,sBACvB,IAAIe,IAAIlB,eACRH,sBACE;YAAEG;YAAaC;QAAQ,GACvB;YAAED,aAAaI,OAAOJ,WAAW;YAAEC,SAASG,OAAOH,OAAO;QAAC;QAEjE,KAAK,MAAMkB,cAAcf,OAAOJ,WAAW,CAAE;YAC3C,IAAI,CAACiB,mBAAmBG,GAAG,CAACD,WAAWN,IAAI,GAAqB;YAChEM,WAAWL,KAAK,KAAK,CAAC;YACtBK,WAAWL,KAAK,CAACC,WAAW,KAAK,EAAE;YACnCI,WAAWL,KAAK,CAACC,WAAW,CAACC,IAAI,CAC/BvB,0BAA0B;gBAAEe;gBAAOC;YAAqB;YAE1DU,WAAWL,KAAK,CAACO,WAAW,KAAK,EAAE;YACnCF,WAAWL,KAAK,CAACO,WAAW,CAACL,IAAI,CAC/BtB,kCAAkC;gBAChCc;gBACAC;YACF;QAEJ;QAEA,OAAOL;IACT,EAAE;AAEJ,SAASM,yBAAyB,EAChCV,WAAW,EACXC,OAAO,EACPC,YAAY,EAIb;IACC,IAAI,CAACA,cAAc,OAAO,IAAM,KAAK;IAErC,MAAMoB,wBAAwB,IAAIJ,IAAIlB;IACtC,MAAMuB,oBAAoB,IAAIL,IAAIjB;IAElC,OAAO,CAACuB;QACN,IAAIA,OAAOC,IAAI,KAAK,gBAAgB,CAACH,sBAAsBF,GAAG,CAACI,OAAOX,IAAI,GACxE;QACF,IAAIW,OAAOC,IAAI,KAAK,YAAY,CAACF,kBAAkBH,GAAG,CAACI,OAAOX,IAAI,GAAG;QACrE,OAAOX,aAAasB;IACtB;AACF"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { CollectionSlug, GlobalSlug, Plugin } from 'payload';\nimport {\n invalidateCollectionCache,\n invalidateCollectionCacheOnDelete,\n invalidateGlobalCache,\n} from './hooks';\nimport type {\n DocumentInvalidation,\n DocumentInvalidationCallback,\n ResolvedPluginOptions,\n} from './types';\nimport { createDependencyGraph } from './utils/dependency-graph';\nimport { getTenantScopedCollections } from './utils/tenant-scoped-collections';\nimport { getTrackedCollections } from './utils/tracked-collections';\n\nexport {\n createRequestHandler,\n type RequestHandlerCacheOptions,\n} from './exports/create-request';\nexport {\n createTenantRequestHandler,\n type TenantRequestHandlerOptions,\n} from './exports/create-tenant-request';\nexport type {\n DocumentInvalidation as InvalidationChange,\n DocumentInvalidationCallback as OnInvalidate,\n} from './types';\n\nexport interface SmartCachePluginConfig<\n C extends CollectionSlug = CollectionSlug,\n G extends GlobalSlug = GlobalSlug,\n> {\n /**\n * The collections to track changes for.\n * By default, collections referenced via relationship or upload fields\n * are automatically tracked as well.\n */\n collections?: C[];\n /**\n * The globals to track changes for.\n * Collections referenced by these globals via relationship or upload\n * fields are automatically tracked as well.\n */\n globals?: G[];\n /**\n * Disable automatic tracking of collections referenced by the configured ones.\n * @default false\n */\n disableAutoTracking?: boolean;\n /**\n * Called when cache invalidation is triggered.\n * Only fires for collections/globals explicitly registered in the config.\n */\n onInvalidate?: DocumentInvalidationCallback<C, G>;\n /**\n * Name of the tenant relationship field on your collections.\n * When set, cache invalidation is scoped per-tenant.\n * Collections without this field are treated as shared and use global invalidation.\n * Must match the field name used by `@payloadcms/plugin-multi-tenant` or your custom tenant field.\n */\n tenantField?: string;\n}\n\nexport const smartCachePlugin =\n <\n C extends CollectionSlug = CollectionSlug,\n G extends GlobalSlug = GlobalSlug,\n >({\n collections = [],\n globals = [],\n onInvalidate,\n disableAutoTracking,\n tenantField,\n }: SmartCachePluginConfig<C, G>): Plugin =>\n (config) => {\n if (collections.length + globals.length === 0) {\n console.warn(\n '[payload-smart-cache] No collections or globals are configured to track changes for, hence this plugin will have no effect.',\n );\n return config;\n }\n\n const graph = createDependencyGraph(config);\n\n const invalidationCallback = wrapInvalidationCallback({\n collections,\n globals,\n onInvalidate: onInvalidate as DocumentInvalidationCallback,\n });\n\n config.globals ??= [];\n for (const global of config.globals) {\n if (!globals.includes(global.slug as G)) continue;\n global.hooks ??= {};\n global.hooks.afterChange ??= [];\n global.hooks.afterChange.push(\n invalidateGlobalCache(invalidationCallback),\n );\n }\n\n config.collections ??= [];\n const tenantScopedCollections = getTenantScopedCollections(\n config.collections,\n tenantField,\n );\n const collectionsToTrack = disableAutoTracking\n ? new Set(collections)\n : getTrackedCollections(\n { collections, globals },\n { collections: config.collections, globals: config.globals },\n );\n for (const collection of config.collections) {\n if (!collectionsToTrack.has(collection.slug as CollectionSlug)) continue;\n collection.hooks ??= {};\n collection.hooks.afterChange ??= [];\n collection.hooks.afterChange.push(\n invalidateCollectionCache({\n graph,\n invalidationCallback,\n tenantField,\n tenantScopedCollections,\n }),\n );\n collection.hooks.afterDelete ??= [];\n collection.hooks.afterDelete.push(\n invalidateCollectionCacheOnDelete({\n graph,\n invalidationCallback,\n tenantField,\n tenantScopedCollections,\n }),\n );\n }\n\n return config;\n };\n\nfunction wrapInvalidationCallback({\n collections,\n globals,\n onInvalidate,\n}: ResolvedPluginOptions<\n 'collections' | 'globals',\n 'onInvalidate'\n>): DocumentInvalidationCallback {\n if (!onInvalidate) return () => void 0;\n\n const registeredCollections = new Set(collections);\n const registeredGlobals = new Set(globals);\n\n return (change: DocumentInvalidation) => {\n if (change.type === 'collection' && !registeredCollections.has(change.slug))\n return;\n if (change.type === 'global' && !registeredGlobals.has(change.slug)) return;\n return onInvalidate(change);\n };\n}\n"],"names":["invalidateCollectionCache","invalidateCollectionCacheOnDelete","invalidateGlobalCache","createDependencyGraph","getTenantScopedCollections","getTrackedCollections","createRequestHandler","createTenantRequestHandler","smartCachePlugin","collections","globals","onInvalidate","disableAutoTracking","tenantField","config","length","console","warn","graph","invalidationCallback","wrapInvalidationCallback","global","includes","slug","hooks","afterChange","push","tenantScopedCollections","collectionsToTrack","Set","collection","has","afterDelete","registeredCollections","registeredGlobals","change","type"],"mappings":"AACA,SACEA,yBAAyB,EACzBC,iCAAiC,EACjCC,qBAAqB,QAChB,UAAU;AAMjB,SAASC,qBAAqB,QAAQ,2BAA2B;AACjE,SAASC,0BAA0B,QAAQ,oCAAoC;AAC/E,SAASC,qBAAqB,QAAQ,8BAA8B;AAEpE,SACEC,oBAAoB,QAEf,2BAA2B;AAClC,SACEC,0BAA0B,QAErB,kCAAkC;AAyCzC,OAAO,MAAMC,mBACX,CAGE,EACAC,cAAc,EAAE,EAChBC,UAAU,EAAE,EACZC,YAAY,EACZC,mBAAmB,EACnBC,WAAW,EACkB,GAC/B,CAACC;QACC,IAAIL,YAAYM,MAAM,GAAGL,QAAQK,MAAM,KAAK,GAAG;YAC7CC,QAAQC,IAAI,CACV;YAEF,OAAOH;QACT;QAEA,MAAMI,QAAQf,sBAAsBW;QAEpC,MAAMK,uBAAuBC,yBAAyB;YACpDX;YACAC;YACAC,cAAcA;QAChB;QAEAG,OAAOJ,OAAO,KAAK,EAAE;QACrB,KAAK,MAAMW,UAAUP,OAAOJ,OAAO,CAAE;YACnC,IAAI,CAACA,QAAQY,QAAQ,CAACD,OAAOE,IAAI,GAAQ;YACzCF,OAAOG,KAAK,KAAK,CAAC;YAClBH,OAAOG,KAAK,CAACC,WAAW,KAAK,EAAE;YAC/BJ,OAAOG,KAAK,CAACC,WAAW,CAACC,IAAI,CAC3BxB,sBAAsBiB;QAE1B;QAEAL,OAAOL,WAAW,KAAK,EAAE;QACzB,MAAMkB,0BAA0BvB,2BAC9BU,OAAOL,WAAW,EAClBI;QAEF,MAAMe,qBAAqBhB,sBACvB,IAAIiB,IAAIpB,eACRJ,sBACE;YAAEI;YAAaC;QAAQ,GACvB;YAAED,aAAaK,OAAOL,WAAW;YAAEC,SAASI,OAAOJ,OAAO;QAAC;QAEjE,KAAK,MAAMoB,cAAchB,OAAOL,WAAW,CAAE;YAC3C,IAAI,CAACmB,mBAAmBG,GAAG,CAACD,WAAWP,IAAI,GAAqB;YAChEO,WAAWN,KAAK,KAAK,CAAC;YACtBM,WAAWN,KAAK,CAACC,WAAW,KAAK,EAAE;YACnCK,WAAWN,KAAK,CAACC,WAAW,CAACC,IAAI,CAC/B1B,0BAA0B;gBACxBkB;gBACAC;gBACAN;gBACAc;YACF;YAEFG,WAAWN,KAAK,CAACQ,WAAW,KAAK,EAAE;YACnCF,WAAWN,KAAK,CAACQ,WAAW,CAACN,IAAI,CAC/BzB,kCAAkC;gBAChCiB;gBACAC;gBACAN;gBACAc;YACF;QAEJ;QAEA,OAAOb;IACT,EAAE;AAEJ,SAASM,yBAAyB,EAChCX,WAAW,EACXC,OAAO,EACPC,YAAY,EAIb;IACC,IAAI,CAACA,cAAc,OAAO,IAAM,KAAK;IAErC,MAAMsB,wBAAwB,IAAIJ,IAAIpB;IACtC,MAAMyB,oBAAoB,IAAIL,IAAInB;IAElC,OAAO,CAACyB;QACN,IAAIA,OAAOC,IAAI,KAAK,gBAAgB,CAACH,sBAAsBF,GAAG,CAACI,OAAOZ,IAAI,GACxE;QACF,IAAIY,OAAOC,IAAI,KAAK,YAAY,CAACF,kBAAkBH,GAAG,CAACI,OAAOZ,IAAI,GAAG;QACrE,OAAOZ,aAAawB;IACtB;AACF"}
package/dist/types.d.ts CHANGED
@@ -8,6 +8,7 @@ export type DocumentInvalidation<C extends CollectionSlug = CollectionSlug, G ex
8
8
  type: 'collection';
9
9
  slug: C;
10
10
  docID: DocumentID;
11
+ tenantId?: string;
11
12
  } | {
12
13
  type: 'global';
13
14
  slug: G;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACtE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,GAAG,CAAC;AAEhD,MAAM,MAAM,qBAAqB,CAC/B,GAAG,SAAS,MAAM,sBAAsB,GAAG,MAAM,sBAAsB,EACvE,GAAG,SAAS,MAAM,sBAAsB,GAAG,KAAK,IAC9C,IAAI,CAAC,QAAQ,CAAC,sBAAsB,CAAC,EAAE,GAAG,CAAC,GAC7C,IAAI,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC;AAEpC,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,UAAU,CAAC;AAErD,iEAAiE;AACjE,MAAM,MAAM,oBAAoB,CAC9B,CAAC,SAAS,cAAc,GAAG,cAAc,EACzC,CAAC,SAAS,UAAU,GAAG,UAAU,IAE/B;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC;AAEhC,mDAAmD;AACnD,MAAM,MAAM,4BAA4B,CACtC,CAAC,SAAS,cAAc,GAAG,cAAc,EACzC,CAAC,SAAS,UAAU,GAAG,UAAU,IAC/B,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEjE,8DAA8D;AAC9D,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG;IAC5C,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;CACjC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACtE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,GAAG,CAAC;AAEhD,MAAM,MAAM,qBAAqB,CAC/B,GAAG,SAAS,MAAM,sBAAsB,GAAG,MAAM,sBAAsB,EACvE,GAAG,SAAS,MAAM,sBAAsB,GAAG,KAAK,IAC9C,IAAI,CAAC,QAAQ,CAAC,sBAAsB,CAAC,EAAE,GAAG,CAAC,GAC7C,IAAI,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC;AAEpC,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,UAAU,CAAC;AAErD,iEAAiE;AACjE,MAAM,MAAM,oBAAoB,CAC9B,CAAC,SAAS,cAAc,GAAG,cAAc,EACzC,CAAC,SAAS,UAAU,GAAG,UAAU,IAE/B;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GACrE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC;AAEhC,mDAAmD;AACnD,MAAM,MAAM,4BAA4B,CACtC,CAAC,SAAS,cAAc,GAAG,cAAc,EACzC,CAAC,SAAS,UAAU,GAAG,UAAU,IAC/B,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEjE,8DAA8D;AAC9D,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG;IAC5C,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;CACjC,CAAC"}
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { DocumentID } from '@davincicoding/payload-plugin-kit';\nimport type { CollectionSlug, GlobalSlug, TypeWithID } from 'payload';\nimport type { SmartCachePluginConfig } from '.';\n\nexport type ResolvedPluginOptions<\n Req extends keyof SmartCachePluginConfig = keyof SmartCachePluginConfig,\n Opt extends keyof SmartCachePluginConfig = never,\n> = Pick<Required<SmartCachePluginConfig>, Req> &\n Pick<SmartCachePluginConfig, Opt>;\n\nexport type EntitySlug = CollectionSlug | GlobalSlug;\n\n/** Discriminated union passed to the `onInvalidate` callback. */\nexport type DocumentInvalidation<\n C extends CollectionSlug = CollectionSlug,\n G extends GlobalSlug = GlobalSlug,\n> =\n | { type: 'collection'; slug: C; docID: DocumentID }\n | { type: 'global'; slug: G };\n\n/** Callback type for the `onInvalidate` option. */\nexport type DocumentInvalidationCallback<\n C extends CollectionSlug = CollectionSlug,\n G extends GlobalSlug = GlobalSlug,\n> = (change: DocumentInvalidation<C, G>) => void | Promise<void>;\n\n/** A document with an optional draft/publish status field. */\nexport type DocumentWithStatus = TypeWithID & {\n _status?: 'published' | 'draft';\n};\n"],"names":[],"mappings":"AA0BA,4DAA4D,GAC5D,WAEE"}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { DocumentID } from '@davincicoding/payload-plugin-kit';\nimport type { CollectionSlug, GlobalSlug, TypeWithID } from 'payload';\nimport type { SmartCachePluginConfig } from '.';\n\nexport type ResolvedPluginOptions<\n Req extends keyof SmartCachePluginConfig = keyof SmartCachePluginConfig,\n Opt extends keyof SmartCachePluginConfig = never,\n> = Pick<Required<SmartCachePluginConfig>, Req> &\n Pick<SmartCachePluginConfig, Opt>;\n\nexport type EntitySlug = CollectionSlug | GlobalSlug;\n\n/** Discriminated union passed to the `onInvalidate` callback. */\nexport type DocumentInvalidation<\n C extends CollectionSlug = CollectionSlug,\n G extends GlobalSlug = GlobalSlug,\n> =\n | { type: 'collection'; slug: C; docID: DocumentID; tenantId?: string }\n | { type: 'global'; slug: G };\n\n/** Callback type for the `onInvalidate` option. */\nexport type DocumentInvalidationCallback<\n C extends CollectionSlug = CollectionSlug,\n G extends GlobalSlug = GlobalSlug,\n> = (change: DocumentInvalidation<C, G>) => void | Promise<void>;\n\n/** A document with an optional draft/publish status field. */\nexport type DocumentWithStatus = TypeWithID & {\n _status?: 'published' | 'draft';\n};\n"],"names":[],"mappings":"AA0BA,4DAA4D,GAC5D,WAEE"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Extracts the tenant ID from a document.
3
+ * Handles raw ID values (string/number) and populated relationship objects.
4
+ * Returns `undefined` if `tenantField` is not set or the field is absent/null.
5
+ */
6
+ export declare function resolveTenantId(doc: Record<string, unknown>, tenantField: string | undefined): string | undefined;
7
+ //# sourceMappingURL=resolve-tenant-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-tenant-id.d.ts","sourceRoot":"","sources":["../../src/utils/resolve-tenant-id.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,MAAM,GAAG,SAAS,CAapB"}
@@ -0,0 +1,20 @@
1
+ function hasId(value) {
2
+ return 'id' in value;
3
+ }
4
+ /**
5
+ * Extracts the tenant ID from a document.
6
+ * Handles raw ID values (string/number) and populated relationship objects.
7
+ * Returns `undefined` if `tenantField` is not set or the field is absent/null.
8
+ */ export function resolveTenantId(doc, tenantField) {
9
+ if (!tenantField) return undefined;
10
+ const value = doc[tenantField];
11
+ if (value == null) return undefined;
12
+ if (typeof value === 'string') return value;
13
+ if (typeof value === 'number') return value.toString();
14
+ if (typeof value === 'object' && hasId(value)) {
15
+ return String(value.id);
16
+ }
17
+ return undefined;
18
+ }
19
+
20
+ //# sourceMappingURL=resolve-tenant-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/resolve-tenant-id.ts"],"sourcesContent":["function hasId(value: object): value is { id: unknown } {\n return 'id' in value;\n}\n\n/**\n * Extracts the tenant ID from a document.\n * Handles raw ID values (string/number) and populated relationship objects.\n * Returns `undefined` if `tenantField` is not set or the field is absent/null.\n */\nexport function resolveTenantId(\n doc: Record<string, unknown>,\n tenantField: string | undefined,\n): string | undefined {\n if (!tenantField) return undefined;\n\n const value = doc[tenantField];\n if (value == null) return undefined;\n\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return value.toString();\n if (typeof value === 'object' && hasId(value)) {\n return String(value.id);\n }\n\n return undefined;\n}\n"],"names":["hasId","value","resolveTenantId","doc","tenantField","undefined","toString","String","id"],"mappings":"AAAA,SAASA,MAAMC,KAAa;IAC1B,OAAO,QAAQA;AACjB;AAEA;;;;CAIC,GACD,OAAO,SAASC,gBACdC,GAA4B,EAC5BC,WAA+B;IAE/B,IAAI,CAACA,aAAa,OAAOC;IAEzB,MAAMJ,QAAQE,GAAG,CAACC,YAAY;IAC9B,IAAIH,SAAS,MAAM,OAAOI;IAE1B,IAAI,OAAOJ,UAAU,UAAU,OAAOA;IACtC,IAAI,OAAOA,UAAU,UAAU,OAAOA,MAAMK,QAAQ;IACpD,IAAI,OAAOL,UAAU,YAAYD,MAAMC,QAAQ;QAC7C,OAAOM,OAAON,MAAMO,EAAE;IACxB;IAEA,OAAOH;AACT"}
@@ -0,0 +1,11 @@
1
+ import type { CollectionConfig, CollectionSlug } from 'payload';
2
+ /**
3
+ * Determines which collections have the tenant field by scanning their field configs.
4
+ * Returns a Set of collection slugs that are tenant-tenantScopedSlugs.
5
+ *
6
+ * Only matches top-level field names (or fields inside unnamed containers like
7
+ * unnamed tabs/rows). Fields nested inside named groups/tabs don't match because
8
+ * the multi-tenant plugin adds the tenant field at the top level.
9
+ */
10
+ export declare function getTenantScopedCollections(collections: CollectionConfig[], tenantField: string | undefined): Set<CollectionSlug>;
11
+ //# sourceMappingURL=tenant-scoped-collections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-scoped-collections.d.ts","sourceRoot":"","sources":["../../src/utils/tenant-scoped-collections.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAS,MAAM,SAAS,CAAC;AAEvE;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,gBAAgB,EAAE,EAC/B,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,GAAG,CAAC,cAAc,CAAC,CAqBrB"}
@@ -0,0 +1,25 @@
1
+ import { findFields } from '@davincicoding/payload-plugin-kit';
2
+ /**
3
+ * Determines which collections have the tenant field by scanning their field configs.
4
+ * Returns a Set of collection slugs that are tenant-tenantScopedSlugs.
5
+ *
6
+ * Only matches top-level field names (or fields inside unnamed containers like
7
+ * unnamed tabs/rows). Fields nested inside named groups/tabs don't match because
8
+ * the multi-tenant plugin adds the tenant field at the top level.
9
+ */ export function getTenantScopedCollections(collections, tenantField) {
10
+ if (!tenantField) return new Set();
11
+ const tenantScopedSlugs = new Set();
12
+ for (const collection of collections){
13
+ const match = findFields(collection.fields, (f)=>{
14
+ return 'name' in f && f.name === tenantField;
15
+ });
16
+ // Only consider fields whose path is exactly [tenantField] — i.e. top-level
17
+ const hasTopLevelTenantField = match.some((f)=>f.path.length === 1 && f.path[0] === tenantField);
18
+ if (hasTopLevelTenantField) {
19
+ tenantScopedSlugs.add(collection.slug);
20
+ }
21
+ }
22
+ return tenantScopedSlugs;
23
+ }
24
+
25
+ //# sourceMappingURL=tenant-scoped-collections.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/tenant-scoped-collections.ts"],"sourcesContent":["import { findFields } from '@davincicoding/payload-plugin-kit';\nimport type { CollectionConfig, CollectionSlug, Field } from 'payload';\n\n/**\n * Determines which collections have the tenant field by scanning their field configs.\n * Returns a Set of collection slugs that are tenant-tenantScopedSlugs.\n *\n * Only matches top-level field names (or fields inside unnamed containers like\n * unnamed tabs/rows). Fields nested inside named groups/tabs don't match because\n * the multi-tenant plugin adds the tenant field at the top level.\n */\nexport function getTenantScopedCollections(\n collections: CollectionConfig[],\n tenantField: string | undefined,\n): Set<CollectionSlug> {\n if (!tenantField) return new Set();\n\n const tenantScopedSlugs = new Set<CollectionSlug>();\n\n for (const collection of collections) {\n const match = findFields(collection.fields, (f: Field) => {\n return 'name' in f && f.name === tenantField;\n });\n\n // Only consider fields whose path is exactly [tenantField] — i.e. top-level\n const hasTopLevelTenantField = match.some(\n (f) => f.path.length === 1 && f.path[0] === tenantField,\n );\n\n if (hasTopLevelTenantField) {\n tenantScopedSlugs.add(collection.slug as CollectionSlug);\n }\n }\n\n return tenantScopedSlugs;\n}\n"],"names":["findFields","getTenantScopedCollections","collections","tenantField","Set","tenantScopedSlugs","collection","match","fields","f","name","hasTopLevelTenantField","some","path","length","add","slug"],"mappings":"AAAA,SAASA,UAAU,QAAQ,oCAAoC;AAG/D;;;;;;;CAOC,GACD,OAAO,SAASC,2BACdC,WAA+B,EAC/BC,WAA+B;IAE/B,IAAI,CAACA,aAAa,OAAO,IAAIC;IAE7B,MAAMC,oBAAoB,IAAID;IAE9B,KAAK,MAAME,cAAcJ,YAAa;QACpC,MAAMK,QAAQP,WAAWM,WAAWE,MAAM,EAAE,CAACC;YAC3C,OAAO,UAAUA,KAAKA,EAAEC,IAAI,KAAKP;QACnC;QAEA,4EAA4E;QAC5E,MAAMQ,yBAAyBJ,MAAMK,IAAI,CACvC,CAACH,IAAMA,EAAEI,IAAI,CAACC,MAAM,KAAK,KAAKL,EAAEI,IAAI,CAAC,EAAE,KAAKV;QAG9C,IAAIQ,wBAAwB;YAC1BN,kBAAkBU,GAAG,CAACT,WAAWU,IAAI;QACvC;IACF;IAEA,OAAOX;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-smart-cache",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Payload Plugin for Cached Data",
5
5
  "keywords": [
6
6
  "payload",
@@ -21,6 +21,11 @@
21
21
  "types": "./dist/index.d.ts",
22
22
  "import": "./dist/index.js",
23
23
  "default": "./dist/index.js"
24
+ },
25
+ "./cache": {
26
+ "types": "./dist/exports/cache.d.ts",
27
+ "import": "./dist/exports/cache.js",
28
+ "default": "./dist/exports/cache.js"
24
29
  }
25
30
  },
26
31
  "main": "./dist/index.js",
@@ -31,7 +36,7 @@
31
36
  "dependencies": {
32
37
  "graph-data-structure": "^4.5.0",
33
38
  "lodash-es": "^4.17.21",
34
- "@davincicoding/payload-plugin-kit": "0.0.7"
39
+ "@davincicoding/payload-plugin-kit": "0.1.0"
35
40
  },
36
41
  "devDependencies": {
37
42
  "@types/lodash-es": "^4.17.12",
@@ -55,11 +60,11 @@
55
60
  },
56
61
  "scripts": {
57
62
  "prebuild": "pnpm check:types",
58
- "build": "plugin-build",
63
+ "build": "plugin-kit build",
59
64
  "check:types": "tsc --noEmit",
60
65
  "dev": "cd dev && pnpm dev",
61
- "dev:build": "plugin-build --watch",
62
- "generate:types": "generate-types",
66
+ "dev:build": "plugin-kit build --watch",
67
+ "generate:types": "plugin-kit generate-types",
63
68
  "lint": "biome check .",
64
69
  "lint:fix": "biome check --write .",
65
70
  "test": "vitest run",