@qkix/strapi-plugin-rewind 0.1.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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +182 -0
  3. package/dist/_chunks/en-BHfx2OWt.js +1 -0
  4. package/dist/_chunks/en-DCctmu-a.mjs +7 -0
  5. package/dist/admin/index.js +1 -0
  6. package/dist/admin/index.mjs +254 -0
  7. package/dist/admin/src/components/ChangesDialog.d.ts +21 -0
  8. package/dist/admin/src/components/HistoryPanel.d.ts +17 -0
  9. package/dist/admin/src/diffRegistry.d.ts +45 -0
  10. package/dist/admin/src/index.d.ts +18 -0
  11. package/dist/admin/src/pluginId.d.ts +6 -0
  12. package/dist/server/index.js +2 -0
  13. package/dist/server/index.mjs +930 -0
  14. package/dist/server/src/bootstrap.d.ts +5 -0
  15. package/dist/server/src/config/index.d.ts +58 -0
  16. package/dist/server/src/content-types/index.d.ts +95 -0
  17. package/dist/server/src/controllers/index.d.ts +12 -0
  18. package/dist/server/src/controllers/version.d.ts +11 -0
  19. package/dist/server/src/destroy.d.ts +9 -0
  20. package/dist/server/src/index.d.ts +166 -0
  21. package/dist/server/src/routes/admin.d.ts +16 -0
  22. package/dist/server/src/routes/index.d.ts +14 -0
  23. package/dist/server/src/services/diff.d.ts +55 -0
  24. package/dist/server/src/services/index.d.ts +35 -0
  25. package/dist/server/src/services/lock.d.ts +20 -0
  26. package/dist/server/src/services/restore.d.ts +44 -0
  27. package/dist/server/src/services/retention.d.ts +18 -0
  28. package/dist/server/src/services/serializer.d.ts +65 -0
  29. package/dist/server/src/services/snapshot.d.ts +17 -0
  30. package/dist/server/src/utils/backfillLabels.d.ts +18 -0
  31. package/dist/server/src/utils/captureContext.d.ts +45 -0
  32. package/dist/server/src/utils/persistTable.d.ts +15 -0
  33. package/dist/server/src/utils/textDiff.d.ts +50 -0
  34. package/dist/server/src/utils/thinning.d.ts +35 -0
  35. package/dist/server/src/utils/versionLabel.d.ts +14 -0
  36. package/package.json +115 -0
@@ -0,0 +1,45 @@
1
+ export type Origin = 'create' | 'update' | 'clone' | 'publish' | 'unpublish' | 'discardDraft' | 'restore';
2
+ export interface SnapshotIntent {
3
+ uid: string;
4
+ relatedDocumentId: string;
5
+ locale: string | null;
6
+ origin: Origin;
7
+ /** Rows read before the action ran. Only `discardDraft` needs these. */
8
+ before: Record<string, unknown>[] | null;
9
+ }
10
+ /**
11
+ * Intents buffered against the transaction they belong to.
12
+ *
13
+ * Keyed by the Knex transaction object because that is the only thing the
14
+ * Content Manager's `update` and `publish` calls have in common — they are
15
+ * sequential siblings, not nested, so nothing about the call stack relates
16
+ * them. Strapi hands the same `trx` to every nested `db.transaction`, which
17
+ * makes it a reliable key, and a WeakMap means a finished transaction takes its
18
+ * buffer with it.
19
+ *
20
+ * Deliberately not an AsyncLocalStorage: commit callbacks run after the
21
+ * transaction resolves, outside whatever async context registered them, so a
22
+ * store looked up at flush time is always empty. The callback has to close over
23
+ * the buffer directly.
24
+ */
25
+ interface TransactionBuffer {
26
+ intents: SnapshotIntent[];
27
+ flushRegistered: boolean;
28
+ }
29
+ export declare const bufferFor: (trx: object) => TransactionBuffer;
30
+ /**
31
+ * Collapses intents that describe the same document and locale, keeping the one
32
+ * that best describes what the editor did.
33
+ */
34
+ export declare const coalesce: (intents: SnapshotIntent[]) => SnapshotIntent[];
35
+ /**
36
+ * Marks the current async context as a restore, so the write restore performs
37
+ * is not also recorded as an ordinary edit.
38
+ *
39
+ * An async-context flag is the right tool here and the wrong one for publish:
40
+ * `restore.apply()` calls the Document Service from inside its own call stack,
41
+ * so the write genuinely is nested. The Content Manager's publish is not.
42
+ */
43
+ export declare const runAsRestore: <T>(fn: () => Promise<T>) => Promise<T>;
44
+ export declare const isRestoreInProgress: () => boolean;
45
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { Core } from '@strapi/strapi';
2
+ /**
3
+ * Keeps the versions table alive when the plugin is not loaded.
4
+ *
5
+ * Strapi's schema sync drops any table it previously tracked that is no longer
6
+ * in the user schema. Disable this plugin for a single boot — to debug, to test
7
+ * an upgrade — and the history it exists to protect is gone without a prompt.
8
+ *
9
+ * `persisted_tables` is the core-store key `@strapi/database` consults before
10
+ * dropping (schema/diff.js). The EE `admin::persist-tables` service writes the
11
+ * same key, but it is registered only when `strapi.EE` is set, so it does not
12
+ * exist for the Community Edition users this plugin is for. Writing the key
13
+ * directly is a dozen lines and needs no EE code.
14
+ */
15
+ export declare const persistVersionsTable: (strapi: Core.Strapi, tableName: string) => Promise<void>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A word-level diff, and the text extraction that makes it useful.
3
+ *
4
+ * The alternative — reporting "content changed" — is true and worthless. The
5
+ * field an editor cares most about is usually rich text stored as JSON, so the
6
+ * readable part is pulled out and compared as prose. That is renderer-agnostic:
7
+ * it walks the structure collecting anything that reads as text, without
8
+ * knowing what a block is.
9
+ */
10
+ export type DiffOp = 'equal' | 'added' | 'removed';
11
+ export interface DiffSpan {
12
+ op: DiffOp;
13
+ value: string;
14
+ }
15
+ /**
16
+ * Above this, a full LCS table costs more memory and time than the answer is
17
+ * worth. The caller falls back to reporting that the field changed.
18
+ */
19
+ export declare const MAX_DIFF_TOKENS = 2500;
20
+ /**
21
+ * Longest common subsequence over words.
22
+ *
23
+ * Word-level rather than character-level because the output is read by a person
24
+ * deciding whether an edit was significant, and a character diff of prose is
25
+ * noise.
26
+ */
27
+ export declare const diffWords: (before: string, after: string) => DiffSpan[];
28
+ /**
29
+ * Pulls the human-readable text out of an arbitrary value.
30
+ *
31
+ * Rich text, components and dynamic zones are all nested objects and arrays
32
+ * whose shape this plugin deliberately does not model — a `text` key here, a
33
+ * `children` array there. Collecting every string reached is imprecise but
34
+ * stable across editors, and it is what turns "content changed" into a sentence
35
+ * an editor recognises.
36
+ */
37
+ export declare const extractText: (value: unknown, depth?: number) => string;
38
+ /** Collapses runs of whitespace so formatting-only edits do not read as changes. */
39
+ export declare const normaliseText: (text: string) => string;
40
+ /** Words of unchanged text kept either side of a change, for context. */
41
+ export declare const CONTEXT_WORDS = 6;
42
+ /**
43
+ * Drops the long stretches of unchanged text between changes.
44
+ *
45
+ * A one-word edit in a long article otherwise renders as the whole article with
46
+ * a single green word somewhere inside it, and the reader has to hunt for the
47
+ * thing the view exists to show. Unchanged runs are trimmed to a few words of
48
+ * context and marked with an ellipsis.
49
+ */
50
+ export declare const collapseEqualSpans: (spans: DiffSpan[], contextWords?: number) => DiffSpan[];
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Deciding which versions to throw away.
3
+ *
4
+ * Kept as a pure function, and tested harder than anything else in the plugin,
5
+ * because it is the only code here that destroys data. Everything else can be
6
+ * wrong and merely unhelpful.
7
+ *
8
+ * The shape of the policy is "recent history in full, older history thinned":
9
+ * an editor looking at last week wants every save, and an editor looking at
10
+ * last year wants a handful of points, not four hundred.
11
+ */
12
+ export interface RetentionPolicy {
13
+ /** Below this age nothing is touched. */
14
+ keepAllDays: number;
15
+ /** Up to this age, one version per calendar day survives. */
16
+ dailyUntilDays: number;
17
+ /** Older than that, one per ISO week survives. */
18
+ maxAgeDays: number;
19
+ /** Whether publish/unpublish/discardDraft/restore are exempt entirely. */
20
+ keepAnchors: boolean;
21
+ }
22
+ export declare const DEFAULT_POLICY: RetentionPolicy;
23
+ export interface PrunableVersion {
24
+ id: number;
25
+ createdAt: string | Date;
26
+ origin: string;
27
+ pinned?: boolean | null;
28
+ }
29
+ /**
30
+ * Ids to delete, given every version of one document in one locale.
31
+ *
32
+ * Takes the whole set for a document rather than a page of rows: "the newest
33
+ * version that day" is not a question a single row can answer.
34
+ */
35
+ export declare const selectExpendable: (versions: PrunableVersion[], now: Date, policy?: RetentionPolicy) => number[];
@@ -0,0 +1,14 @@
1
+ import type { Core } from '@strapi/strapi';
2
+ /**
3
+ * Derives the short, human-readable stand-in shown for a version in the panel.
4
+ *
5
+ * Without it every row is a badge and a timestamp, so two versions holding
6
+ * completely different content look identical and nothing tells an editor what
7
+ * pressing Restore would give them.
8
+ *
9
+ * The field is whatever the Content Manager displays as the entry's title, so
10
+ * the panel agrees with the rest of the admin. The returned function caches the
11
+ * lookup per content type: a backfill runs it once per row, and the
12
+ * configuration behind it does not change while the server is up.
13
+ */
14
+ export declare const createLabeller: (strapi: Core.Strapi) => (uid: string, data: Record<string, unknown> | null | undefined) => Promise<string | null>;
package/package.json ADDED
@@ -0,0 +1,115 @@
1
+ {
2
+ "name": "@qkix/strapi-plugin-rewind",
3
+ "version": "0.1.0",
4
+ "description": "Document version history for Strapi v5 — snapshots on every save, with restore",
5
+ "strapi": {
6
+ "kind": "plugin",
7
+ "name": "rewind",
8
+ "displayName": "Rewind",
9
+ "description": "Document version history for Strapi v5 — snapshots on every save, with restore"
10
+ },
11
+ "keywords": [
12
+ "strapi",
13
+ "strapi-plugin",
14
+ "strapi-v5",
15
+ "history",
16
+ "versioning",
17
+ "versions",
18
+ "revisions",
19
+ "audit",
20
+ "restore",
21
+ "cms",
22
+ "headless-cms"
23
+ ],
24
+ "type": "commonjs",
25
+ "exports": {
26
+ "./package.json": "./package.json",
27
+ "./strapi-admin": {
28
+ "types": "./dist/admin/src/index.d.ts",
29
+ "source": "./admin/src/index.ts",
30
+ "import": "./dist/admin/index.mjs",
31
+ "require": "./dist/admin/index.js",
32
+ "default": "./dist/admin/index.js"
33
+ },
34
+ "./strapi-server": {
35
+ "types": "./dist/server/src/index.d.ts",
36
+ "source": "./server/src/index.ts",
37
+ "import": "./dist/server/index.mjs",
38
+ "require": "./dist/server/index.js",
39
+ "default": "./dist/server/index.js"
40
+ }
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "devDependencies": {
46
+ "@strapi/sdk-plugin": "^5.3.2",
47
+ "@strapi/strapi": "^5.12.5",
48
+ "@strapi/typescript-utils": "^5.12.5",
49
+ "@types/react": "^18.3.8",
50
+ "@types/react-dom": "^18.3.0",
51
+ "eslint": "^10.8.1",
52
+ "prettier": "^3.5.3",
53
+ "react": "^18.3.1",
54
+ "react-dom": "^18.3.1",
55
+ "styled-components": "^6.1.17",
56
+ "typescript": "^5.8.3",
57
+ "vitest": "^3.2.4",
58
+ "@qkix/eslint-config": "0.0.0",
59
+ "@qkix/strapi-test-harness": "0.0.0",
60
+ "@qkix/tsconfig": "0.0.0"
61
+ },
62
+ "peerDependencies": {
63
+ "@strapi/admin": "^5.0.0",
64
+ "@strapi/design-system": "^2.0.0",
65
+ "@strapi/icons": "^2.0.0",
66
+ "@strapi/strapi": "^5.0.0",
67
+ "react": "^17.0.0 || ^18.0.0",
68
+ "react-dom": "^17.0.0 || ^18.0.0",
69
+ "react-intl": "^6.0.0 || ^7.0.0",
70
+ "styled-components": "^6.0.0"
71
+ },
72
+ "engines": {
73
+ "node": ">=20.0.0 <=24.x.x",
74
+ "npm": ">=6.0.0"
75
+ },
76
+ "license": "MIT",
77
+ "author": "qkix",
78
+ "repository": {
79
+ "type": "git",
80
+ "url": "git+https://github.com/qkix/strapi-plugins.git",
81
+ "directory": "packages/strapi-plugin-rewind"
82
+ },
83
+ "bugs": {
84
+ "url": "https://github.com/qkix/strapi-plugins/issues"
85
+ },
86
+ "homepage": "https://github.com/qkix/strapi-plugins#readme",
87
+ "nx": {
88
+ "tags": [
89
+ "scope:plugin"
90
+ ],
91
+ "targets": {
92
+ "test": {
93
+ "dependsOn": [
94
+ "build",
95
+ "^build"
96
+ ]
97
+ }
98
+ }
99
+ },
100
+ "publishConfig": {
101
+ "access": "public"
102
+ },
103
+ "scripts": {
104
+ "build": "rm -rf dist && strapi-plugin build --minify",
105
+ "clean": "rm -rf dist",
106
+ "lint": "eslint . && prettier --check .",
107
+ "format": "prettier --write .",
108
+ "test": "vitest run",
109
+ "test:watch": "vitest",
110
+ "verify": "strapi-plugin verify",
111
+ "watch": "strapi-plugin watch",
112
+ "watch:link": "strapi-plugin watch:link",
113
+ "typecheck": "tsc -p admin/tsconfig.json --noEmit && tsc -p server/tsconfig.json --noEmit"
114
+ }
115
+ }