@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.
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/dist/_chunks/en-BHfx2OWt.js +1 -0
- package/dist/_chunks/en-DCctmu-a.mjs +7 -0
- package/dist/admin/index.js +1 -0
- package/dist/admin/index.mjs +254 -0
- package/dist/admin/src/components/ChangesDialog.d.ts +21 -0
- package/dist/admin/src/components/HistoryPanel.d.ts +17 -0
- package/dist/admin/src/diffRegistry.d.ts +45 -0
- package/dist/admin/src/index.d.ts +18 -0
- package/dist/admin/src/pluginId.d.ts +6 -0
- package/dist/server/index.js +2 -0
- package/dist/server/index.mjs +930 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +58 -0
- package/dist/server/src/content-types/index.d.ts +95 -0
- package/dist/server/src/controllers/index.d.ts +12 -0
- package/dist/server/src/controllers/version.d.ts +11 -0
- package/dist/server/src/destroy.d.ts +9 -0
- package/dist/server/src/index.d.ts +166 -0
- package/dist/server/src/routes/admin.d.ts +16 -0
- package/dist/server/src/routes/index.d.ts +14 -0
- package/dist/server/src/services/diff.d.ts +55 -0
- package/dist/server/src/services/index.d.ts +35 -0
- package/dist/server/src/services/lock.d.ts +20 -0
- package/dist/server/src/services/restore.d.ts +44 -0
- package/dist/server/src/services/retention.d.ts +18 -0
- package/dist/server/src/services/serializer.d.ts +65 -0
- package/dist/server/src/services/snapshot.d.ts +17 -0
- package/dist/server/src/utils/backfillLabels.d.ts +18 -0
- package/dist/server/src/utils/captureContext.d.ts +45 -0
- package/dist/server/src/utils/persistTable.d.ts +15 -0
- package/dist/server/src/utils/textDiff.d.ts +50 -0
- package/dist/server/src/utils/thinning.d.ts +35 -0
- package/dist/server/src/utils/versionLabel.d.ts +14 -0
- 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
|
+
}
|