@easy-cms/plugin-redirects 0.44.0 → 0.45.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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { TypedPlugin } from '@easy-cms/core';
1
+ import { TypedPlugin, RequestContext } from '@easy-cms/core';
2
2
 
3
3
  type Doc$1 = Record<string, unknown>;
4
4
  interface UrlArgs {
@@ -92,6 +92,9 @@ interface RedirectsCMS {
92
92
  readonly handler: unknown;
93
93
  }[];
94
94
  };
95
+ uniqueScope?(collection: string, field: string, options?: {
96
+ context?: RequestContext;
97
+ }): Promise<Record<string, unknown>>;
95
98
  find(collection: string, options: Record<string, unknown>): Promise<{
96
99
  docs: Doc[];
97
100
  hasNextPage: boolean;
@@ -108,6 +111,13 @@ interface Redirect {
108
111
  * `null` when it isn't a path.
109
112
  */
110
113
  declare function normalizePath(input: string): string | null;
114
+ interface ResolveRedirectOptions {
115
+ /**
116
+ * The request's context (e.g. its tenant, `cms.forRequest(request)`): with redirects kept per
117
+ * scope (`uniqueWithin` of `from`), only that scope's apply.
118
+ */
119
+ readonly context?: RequestContext;
120
+ }
111
121
  /** Forgets the cached redirects, so the next request reads them again. */
112
122
  declare function clearRedirects(cms: object): void;
113
123
  /**
@@ -120,7 +130,7 @@ declare function clearRedirects(cms: object): void;
120
130
  * if (redirect) return Response.redirect(new URL(redirect.location, request.url), redirect.status)
121
131
  * ```
122
132
  */
123
- declare function resolveRedirect(cms: RedirectsCMS, input: string | URL): Promise<Redirect | null>;
133
+ declare function resolveRedirect(cms: RedirectsCMS, input: string | URL, options?: ResolveRedirectOptions): Promise<Redirect | null>;
124
134
 
125
135
  declare const STATUSES: readonly [301, 302, 307, 308];
126
136
  type RedirectStatus = (typeof STATUSES)[number];
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  // src/info.ts
7
7
  var INFO = {
8
8
  name: "@easy-cms/plugin-redirects",
9
- ...true ? { version: "0.44.0" } : {}
9
+ ...true ? { version: "0.45.0" } : {}
10
10
  };
11
11
 
12
12
  // src/shared.ts
@@ -31,12 +31,12 @@ var CACHE = /* @__PURE__ */ Symbol.for("easy-cms.plugin-redirects.cache");
31
31
  function clearRedirects(cms) {
32
32
  delete cms[CACHE];
33
33
  }
34
- async function resolveRedirect(cms, input) {
34
+ async function resolveRedirect(cms, input, options = {}) {
35
35
  const url = typeof input === "string" ? safeUrl(input) : input;
36
36
  if (!url) return null;
37
37
  const path = normalizePath(url.pathname);
38
38
  if (!path) return null;
39
- const found = (await redirects(cms)).get(path);
39
+ const found = (await redirects(cms, options)).get(path);
40
40
  if (!found) return null;
41
41
  const location = url.search && !found.location.includes("?") ? found.location + url.search : found.location;
42
42
  return { location, status: found.status };
@@ -55,22 +55,33 @@ function findSource(cms) {
55
55
  }
56
56
  throw new Error("resolveRedirect: add redirectsPlugin() to the plugins in your Easy CMS config");
57
57
  }
58
- function redirects(cms) {
58
+ async function redirects(cms, options) {
59
59
  const source = findSource(cms);
60
+ const scope = options.context && cms.uniqueScope ? await cms.uniqueScope(source.slug, "from", { context: options.context }) : {};
61
+ const key = JSON.stringify(scope);
60
62
  const holder = cms;
61
- const cached = holder[CACHE];
63
+ holder[CACHE] ??= /* @__PURE__ */ new Map();
64
+ const caches = holder[CACHE];
65
+ const cached = caches.get(key);
62
66
  if (cached && Date.now() - cached.at < source.cacheTTL) return cached.map;
63
- const map = load(cms, source);
64
- holder[CACHE] = { at: Date.now(), map };
67
+ const map = load(cms, source, scope);
68
+ caches.set(key, { at: Date.now(), map });
65
69
  map.catch(() => {
66
- if (holder[CACHE]?.map === map) delete holder[CACHE];
70
+ if (caches.get(key)?.map === map) caches.delete(key);
67
71
  });
68
72
  return map;
69
73
  }
70
- async function load(cms, source) {
74
+ async function load(cms, source, scope) {
71
75
  const map = /* @__PURE__ */ new Map();
76
+ const where = Object.entries(scope).map(([name, value]) => ({ [name]: { equals: value } }));
72
77
  for (let page = 1; ; page++) {
73
- const result = await cms.find(source.slug, { depth: 0, sort: "id", limit: 500, page });
78
+ const result = await cms.find(source.slug, {
79
+ depth: 0,
80
+ sort: "id",
81
+ limit: 500,
82
+ page,
83
+ ...where.length ? { where: { and: where } } : {}
84
+ });
74
85
  for (const doc of result.docs) {
75
86
  const from = typeof doc.from === "string" ? normalizePath(doc.from) : null;
76
87
  if (!from) continue;
@@ -233,20 +244,30 @@ function redirectsPlugin(options = {}) {
233
244
  const to = now[i];
234
245
  if (!from || !to || from === to || done.has(from)) continue;
235
246
  done.add(from);
236
- await addRedirect(cms, { from, collection: s, id: doc.id, locale, current: to });
247
+ await addRedirect(cms, { from, collection: s, id: doc.id, locale, current: to, doc });
237
248
  }
238
249
  };
239
250
  const addRedirect = async (cms, args) => {
240
- const loops = await cms.find(slug, { where: { from: { equals: args.current } }, limit: 100 });
251
+ const from = cms.config.collections.find((c) => c.slug === slug)?.fields.find((f) => f.name === "from");
252
+ const scope = Object.fromEntries(
253
+ (from?.uniqueWithin === void 0 ? [] : [from.uniqueWithin].flat()).map((name) => [
254
+ name,
255
+ args.doc[name] ?? null
256
+ ])
257
+ );
258
+ const inScope = Object.entries(scope).map(([name, value]) => ({ [name]: { equals: value } }));
259
+ const where = (path) => ({ and: [{ from: { equals: path } }, ...inScope] });
260
+ const loops = await cms.find(slug, { where: where(args.current), limit: 100 });
241
261
  for (const loop of loops.docs) await cms.delete(slug, loop.id);
242
262
  const data = {
243
263
  from: args.from,
244
264
  to: null,
245
265
  [targetField(args.collection)]: args.id,
246
266
  ...locales.length > 0 ? { locale: args.locale } : {},
247
- type: "301"
267
+ type: "301",
268
+ ...scope
248
269
  };
249
- const existing = await cms.find(slug, { where: { from: { equals: args.from } }, limit: 1 });
270
+ const existing = await cms.find(slug, { where: where(args.from), limit: 1 });
250
271
  if (existing.docs[0]) await cms.update(slug, existing.docs[0].id, data);
251
272
  else await cms.create(slug, data);
252
273
  };
@@ -267,11 +288,11 @@ function redirectsPlugin(options = {}) {
267
288
  method: "get",
268
289
  // Lets `resolveRedirect(cms)` find these options from the resolved config.
269
290
  handler: Object.assign(
270
- async ({ url, cms }) => {
291
+ async ({ url, cms, context }) => {
271
292
  const path = url.searchParams.get("path");
272
293
  if (!path)
273
294
  return Response.json({ errors: [{ message: "path is required" }] }, { status: 400 });
274
- const redirect = await resolveRedirect(cms, path);
295
+ const redirect = await resolveRedirect(cms, path, { context });
275
296
  if (!redirect)
276
297
  return Response.json({ errors: [{ message: "No redirect" }] }, { status: 404 });
277
298
  return Response.json(redirect, { headers: { "cache-control": "public, max-age=60" } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/plugin-redirects",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "Redirects for Easy CMS: 301s managed in the admin, automatic redirects when a page's address changes",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,11 +16,11 @@
16
16
  "dist"
17
17
  ],
18
18
  "peerDependencies": {
19
- "@easy-cms/core": "^0.44.0"
19
+ "@easy-cms/core": "^0.45.0"
20
20
  },
21
21
  "devDependencies": {
22
- "@easy-cms/core": "^0.44.0",
23
- "@easy-cms/db-sqlite": "^0.44.0"
22
+ "@easy-cms/core": "^0.45.0",
23
+ "@easy-cms/db-sqlite": "^0.45.0"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=22.12"