@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,58 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
default: {
|
|
3
|
+
/**
|
|
4
|
+
* Content types to version, by uid. Empty means nothing is tracked.
|
|
5
|
+
*
|
|
6
|
+
* The default is deliberate. A plugin that silently starts writing a row on
|
|
7
|
+
* every save the moment it is installed is a plugin that gets uninstalled
|
|
8
|
+
* after the first disk alert.
|
|
9
|
+
*
|
|
10
|
+
* 'rewind': {
|
|
11
|
+
* enabled: true,
|
|
12
|
+
* config: { contentTypes: ['api::article.article'] },
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
contentTypes: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Whether to version writes that do not come from the Content Manager.
|
|
18
|
+
*
|
|
19
|
+
* Strapi's own history ignores them, so `false` keeps the out-of-the-box
|
|
20
|
+
* behaviour familiar. With `true`, REST/GraphQL and programmatic writes are
|
|
21
|
+
* versioned too, with `createdById` left null.
|
|
22
|
+
*
|
|
23
|
+
* Either way this only ever sees the Document Service. Writes made through
|
|
24
|
+
* `strapi.db.query()` or the legacy entity service do not pass through the
|
|
25
|
+
* middleware and cannot be captured.
|
|
26
|
+
*/
|
|
27
|
+
trackApiWrites: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* How long history is kept.
|
|
30
|
+
*
|
|
31
|
+
* On by default, because a version table that only grows is a problem the
|
|
32
|
+
* plugin would be creating rather than solving. The defaults are gentle:
|
|
33
|
+
* a full week of every save, then a version a day for a month, then one a
|
|
34
|
+
* week — and anchors (publish, unpublish, discard, restore) and pinned
|
|
35
|
+
* versions are never removed at all, whatever their age.
|
|
36
|
+
*
|
|
37
|
+
* Set `enabled: false` to keep everything forever and manage it yourself.
|
|
38
|
+
*/
|
|
39
|
+
retention: {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
keepAllDays: number;
|
|
42
|
+
dailyUntilDays: number;
|
|
43
|
+
maxAgeDays: number;
|
|
44
|
+
keepAnchors: boolean;
|
|
45
|
+
};
|
|
46
|
+
/** When the thinning runs. Standard cron, in the server's timezone. */
|
|
47
|
+
cron: string;
|
|
48
|
+
};
|
|
49
|
+
validator(config?: {
|
|
50
|
+
contentTypes?: unknown;
|
|
51
|
+
retention?: {
|
|
52
|
+
keepAllDays?: number;
|
|
53
|
+
dailyUntilDays?: number;
|
|
54
|
+
maxAgeDays?: number;
|
|
55
|
+
};
|
|
56
|
+
}): void;
|
|
57
|
+
};
|
|
58
|
+
export default _default;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The versions table.
|
|
3
|
+
*
|
|
4
|
+
* `relatedDocumentId`, not `documentId`: Strapi injects a `documentId` into
|
|
5
|
+
* every content type and rejects the name outright — "The attribute
|
|
6
|
+
* "documentId" is reserved and cannot be used in a model" — at boot, before
|
|
7
|
+
* anything else runs. Strapi's own history model calls it `relatedDocumentId`
|
|
8
|
+
* for the same reason.
|
|
9
|
+
*
|
|
10
|
+
* It is a plain string rather than a relation on purpose: a cascading delete
|
|
11
|
+
* would erase a document's history at the exact moment it becomes most useful.
|
|
12
|
+
*
|
|
13
|
+
* `userId` is not `createdById` for a related reason: Strapi adds its own
|
|
14
|
+
* `createdBy` relation to every content type, which occupies the `created_by_id`
|
|
15
|
+
* column, and a second attribute snake-casing to the same name fails the table
|
|
16
|
+
* creation outright.
|
|
17
|
+
*/
|
|
18
|
+
declare const _default: {
|
|
19
|
+
version: {
|
|
20
|
+
schema: {
|
|
21
|
+
kind: string;
|
|
22
|
+
collectionName: string;
|
|
23
|
+
info: {
|
|
24
|
+
singularName: string;
|
|
25
|
+
pluralName: string;
|
|
26
|
+
displayName: string;
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
options: {
|
|
30
|
+
draftAndPublish: boolean;
|
|
31
|
+
comment: string;
|
|
32
|
+
};
|
|
33
|
+
pluginOptions: {
|
|
34
|
+
"content-manager": {
|
|
35
|
+
visible: boolean;
|
|
36
|
+
};
|
|
37
|
+
"content-type-builder": {
|
|
38
|
+
visible: boolean;
|
|
39
|
+
};
|
|
40
|
+
i18n: {
|
|
41
|
+
localized: boolean;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
attributes: {
|
|
45
|
+
contentType: {
|
|
46
|
+
type: string;
|
|
47
|
+
required: boolean;
|
|
48
|
+
};
|
|
49
|
+
relatedDocumentId: {
|
|
50
|
+
type: string;
|
|
51
|
+
required: boolean;
|
|
52
|
+
};
|
|
53
|
+
locale: {
|
|
54
|
+
type: string;
|
|
55
|
+
};
|
|
56
|
+
status: {
|
|
57
|
+
type: string;
|
|
58
|
+
enum: string[];
|
|
59
|
+
required: boolean;
|
|
60
|
+
};
|
|
61
|
+
label: {
|
|
62
|
+
type: string;
|
|
63
|
+
};
|
|
64
|
+
data: {
|
|
65
|
+
type: string;
|
|
66
|
+
required: boolean;
|
|
67
|
+
};
|
|
68
|
+
schemaSnapshot: {
|
|
69
|
+
type: string;
|
|
70
|
+
required: boolean;
|
|
71
|
+
};
|
|
72
|
+
relations: {
|
|
73
|
+
type: string;
|
|
74
|
+
};
|
|
75
|
+
contentHash: {
|
|
76
|
+
type: string;
|
|
77
|
+
required: boolean;
|
|
78
|
+
};
|
|
79
|
+
origin: {
|
|
80
|
+
type: string;
|
|
81
|
+
enum: string[];
|
|
82
|
+
required: boolean;
|
|
83
|
+
};
|
|
84
|
+
pinned: {
|
|
85
|
+
type: string;
|
|
86
|
+
default: boolean;
|
|
87
|
+
};
|
|
88
|
+
userId: {
|
|
89
|
+
type: string;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
export default _default;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
version: ({ strapi }: {
|
|
3
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
4
|
+
}) => {
|
|
5
|
+
find(ctx: any): Promise<any>;
|
|
6
|
+
findOne(ctx: any): Promise<any>;
|
|
7
|
+
diff(ctx: any): Promise<any>;
|
|
8
|
+
preview(ctx: any): Promise<any>;
|
|
9
|
+
restore(ctx: any): Promise<any>;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
declare const version: ({ strapi }: {
|
|
3
|
+
strapi: Core.Strapi;
|
|
4
|
+
}) => {
|
|
5
|
+
find(ctx: any): Promise<any>;
|
|
6
|
+
findOne(ctx: any): Promise<any>;
|
|
7
|
+
diff(ctx: any): Promise<any>;
|
|
8
|
+
preview(ctx: any): Promise<any>;
|
|
9
|
+
restore(ctx: any): Promise<any>;
|
|
10
|
+
};
|
|
11
|
+
export default version;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
/**
|
|
3
|
+
* Without this the dev server stacks up a new thinning job on every reload,
|
|
4
|
+
* and they all fire together.
|
|
5
|
+
*/
|
|
6
|
+
declare const destroy: ({ strapi }: {
|
|
7
|
+
strapi: Core.Strapi;
|
|
8
|
+
}) => void;
|
|
9
|
+
export default destroy;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
bootstrap: ({ strapi }: {
|
|
3
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
4
|
+
}) => Promise<void>;
|
|
5
|
+
destroy: ({ strapi }: {
|
|
6
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
7
|
+
}) => void;
|
|
8
|
+
config: {
|
|
9
|
+
default: {
|
|
10
|
+
contentTypes: string[];
|
|
11
|
+
trackApiWrites: boolean;
|
|
12
|
+
retention: {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
keepAllDays: number;
|
|
15
|
+
dailyUntilDays: number;
|
|
16
|
+
maxAgeDays: number;
|
|
17
|
+
keepAnchors: boolean;
|
|
18
|
+
};
|
|
19
|
+
cron: string;
|
|
20
|
+
};
|
|
21
|
+
validator(config?: {
|
|
22
|
+
contentTypes?: unknown;
|
|
23
|
+
retention?: {
|
|
24
|
+
keepAllDays?: number;
|
|
25
|
+
dailyUntilDays?: number;
|
|
26
|
+
maxAgeDays?: number;
|
|
27
|
+
};
|
|
28
|
+
}): void;
|
|
29
|
+
};
|
|
30
|
+
contentTypes: {
|
|
31
|
+
version: {
|
|
32
|
+
schema: {
|
|
33
|
+
kind: string;
|
|
34
|
+
collectionName: string;
|
|
35
|
+
info: {
|
|
36
|
+
singularName: string;
|
|
37
|
+
pluralName: string;
|
|
38
|
+
displayName: string;
|
|
39
|
+
description: string;
|
|
40
|
+
};
|
|
41
|
+
options: {
|
|
42
|
+
draftAndPublish: boolean;
|
|
43
|
+
comment: string;
|
|
44
|
+
};
|
|
45
|
+
pluginOptions: {
|
|
46
|
+
"content-manager": {
|
|
47
|
+
visible: boolean;
|
|
48
|
+
};
|
|
49
|
+
"content-type-builder": {
|
|
50
|
+
visible: boolean;
|
|
51
|
+
};
|
|
52
|
+
i18n: {
|
|
53
|
+
localized: boolean;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
attributes: {
|
|
57
|
+
contentType: {
|
|
58
|
+
type: string;
|
|
59
|
+
required: boolean;
|
|
60
|
+
};
|
|
61
|
+
relatedDocumentId: {
|
|
62
|
+
type: string;
|
|
63
|
+
required: boolean;
|
|
64
|
+
};
|
|
65
|
+
locale: {
|
|
66
|
+
type: string;
|
|
67
|
+
};
|
|
68
|
+
status: {
|
|
69
|
+
type: string;
|
|
70
|
+
enum: string[];
|
|
71
|
+
required: boolean;
|
|
72
|
+
};
|
|
73
|
+
label: {
|
|
74
|
+
type: string;
|
|
75
|
+
};
|
|
76
|
+
data: {
|
|
77
|
+
type: string;
|
|
78
|
+
required: boolean;
|
|
79
|
+
};
|
|
80
|
+
schemaSnapshot: {
|
|
81
|
+
type: string;
|
|
82
|
+
required: boolean;
|
|
83
|
+
};
|
|
84
|
+
relations: {
|
|
85
|
+
type: string;
|
|
86
|
+
};
|
|
87
|
+
contentHash: {
|
|
88
|
+
type: string;
|
|
89
|
+
required: boolean;
|
|
90
|
+
};
|
|
91
|
+
origin: {
|
|
92
|
+
type: string;
|
|
93
|
+
enum: string[];
|
|
94
|
+
required: boolean;
|
|
95
|
+
};
|
|
96
|
+
pinned: {
|
|
97
|
+
type: string;
|
|
98
|
+
default: boolean;
|
|
99
|
+
};
|
|
100
|
+
userId: {
|
|
101
|
+
type: string;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
controllers: {
|
|
108
|
+
version: ({ strapi }: {
|
|
109
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
110
|
+
}) => {
|
|
111
|
+
find(ctx: any): Promise<any>;
|
|
112
|
+
findOne(ctx: any): Promise<any>;
|
|
113
|
+
diff(ctx: any): Promise<any>;
|
|
114
|
+
preview(ctx: any): Promise<any>;
|
|
115
|
+
restore(ctx: any): Promise<any>;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
routes: {
|
|
119
|
+
admin: {
|
|
120
|
+
type: string;
|
|
121
|
+
routes: {
|
|
122
|
+
method: string;
|
|
123
|
+
path: string;
|
|
124
|
+
handler: string;
|
|
125
|
+
config: {
|
|
126
|
+
policies: string[];
|
|
127
|
+
};
|
|
128
|
+
}[];
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
services: {
|
|
132
|
+
snapshot: ({ strapi }: {
|
|
133
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
134
|
+
}) => {
|
|
135
|
+
readRows: (uid: string, relatedDocumentId: string, locales: string[]) => Promise<Record<string, unknown>[]>;
|
|
136
|
+
captureAll(intents: import("./utils/captureContext").SnapshotIntent[], userId: number): Promise<void>;
|
|
137
|
+
capture(intent: import("./utils/captureContext").SnapshotIntent, userId: number): Promise<void>;
|
|
138
|
+
};
|
|
139
|
+
restore: ({ strapi }: {
|
|
140
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
141
|
+
}) => {
|
|
142
|
+
preview(versionId: number): Promise<import("./services/restore").RestorePreview>;
|
|
143
|
+
apply(versionId: number, userId: number): Promise<{
|
|
144
|
+
documentId: string;
|
|
145
|
+
warnings: string[];
|
|
146
|
+
}>;
|
|
147
|
+
};
|
|
148
|
+
diff: ({ strapi }: {
|
|
149
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
150
|
+
}) => {
|
|
151
|
+
between(versionId: number, againstId?: number): Promise<import("./services/diff").VersionDiff>;
|
|
152
|
+
};
|
|
153
|
+
retention: ({ strapi }: {
|
|
154
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
155
|
+
}) => {
|
|
156
|
+
prune(now?: Date): Promise<import("./services/retention").PruneResult>;
|
|
157
|
+
};
|
|
158
|
+
lock: ({ strapi }: {
|
|
159
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
160
|
+
}) => {
|
|
161
|
+
acquire(name: string, ttlMs: number): Promise<boolean>;
|
|
162
|
+
release(name: string): Promise<void>;
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
export default _default;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read and restore are separate permissions on purpose: a reviewer should be
|
|
3
|
+
* able to see what changed without being able to undo someone else's work.
|
|
4
|
+
*/
|
|
5
|
+
declare const _default: {
|
|
6
|
+
type: string;
|
|
7
|
+
routes: {
|
|
8
|
+
method: string;
|
|
9
|
+
path: string;
|
|
10
|
+
handler: string;
|
|
11
|
+
config: {
|
|
12
|
+
policies: string[];
|
|
13
|
+
};
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
import type { RelationRef } from './serializer';
|
|
3
|
+
import { type DiffSpan } from '../utils/textDiff';
|
|
4
|
+
export type ChangeKind = 'added' | 'removed' | 'changed';
|
|
5
|
+
export interface FieldChange {
|
|
6
|
+
field: string;
|
|
7
|
+
/** The attribute's type in the live model, or 'unknown' if it is gone. */
|
|
8
|
+
type: string;
|
|
9
|
+
/**
|
|
10
|
+
* A custom field's uid, when the attribute is one. Custom fields all report
|
|
11
|
+
* their storage type — usually `json` — so this is the only thing specific
|
|
12
|
+
* enough for the owning package to key a diff renderer on.
|
|
13
|
+
*/
|
|
14
|
+
customField?: string;
|
|
15
|
+
kind: ChangeKind;
|
|
16
|
+
/** Scalars only — the values themselves, for a plain before/after. */
|
|
17
|
+
before?: unknown;
|
|
18
|
+
after?: unknown;
|
|
19
|
+
/** Prose fields — a word-level diff of the readable text. */
|
|
20
|
+
spans?: DiffSpan[];
|
|
21
|
+
/** Relations and media — what was linked and unlinked. */
|
|
22
|
+
linked?: RelationRef[];
|
|
23
|
+
unlinked?: RelationRef[];
|
|
24
|
+
}
|
|
25
|
+
export interface VersionDiff {
|
|
26
|
+
from: {
|
|
27
|
+
id: number;
|
|
28
|
+
label: string | null;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
origin: string;
|
|
31
|
+
} | null;
|
|
32
|
+
to: {
|
|
33
|
+
id: number;
|
|
34
|
+
label: string | null;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
origin: string;
|
|
37
|
+
};
|
|
38
|
+
changes: FieldChange[];
|
|
39
|
+
/** True when the two versions hold identical content. */
|
|
40
|
+
identical: boolean;
|
|
41
|
+
}
|
|
42
|
+
declare const diff: ({ strapi }: {
|
|
43
|
+
strapi: Core.Strapi;
|
|
44
|
+
}) => {
|
|
45
|
+
/**
|
|
46
|
+
* What changed between a version and the one before it.
|
|
47
|
+
*
|
|
48
|
+
* Comparing against the previous version — rather than against the document
|
|
49
|
+
* as it stands — is what makes a history readable: each entry answers "what
|
|
50
|
+
* did this edit do?". The other question, "what would restoring this
|
|
51
|
+
* change?", is what `restore.preview()` already answers.
|
|
52
|
+
*/
|
|
53
|
+
between(versionId: number, againstId?: number): Promise<VersionDiff>;
|
|
54
|
+
};
|
|
55
|
+
export default diff;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
snapshot: ({ strapi }: {
|
|
3
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
4
|
+
}) => {
|
|
5
|
+
readRows: (uid: string, relatedDocumentId: string, locales: string[]) => Promise<Record<string, unknown>[]>;
|
|
6
|
+
captureAll(intents: import("../utils/captureContext").SnapshotIntent[], userId: number): Promise<void>;
|
|
7
|
+
capture(intent: import("../utils/captureContext").SnapshotIntent, userId: number): Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
restore: ({ strapi }: {
|
|
10
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
11
|
+
}) => {
|
|
12
|
+
preview(versionId: number): Promise<import("./restore").RestorePreview>;
|
|
13
|
+
apply(versionId: number, userId: number): Promise<{
|
|
14
|
+
documentId: string;
|
|
15
|
+
warnings: string[];
|
|
16
|
+
}>;
|
|
17
|
+
};
|
|
18
|
+
diff: ({ strapi }: {
|
|
19
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
20
|
+
}) => {
|
|
21
|
+
between(versionId: number, againstId?: number): Promise<import("./diff").VersionDiff>;
|
|
22
|
+
};
|
|
23
|
+
retention: ({ strapi }: {
|
|
24
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
25
|
+
}) => {
|
|
26
|
+
prune(now?: Date): Promise<import("./retention").PruneResult>;
|
|
27
|
+
};
|
|
28
|
+
lock: ({ strapi }: {
|
|
29
|
+
strapi: import("@strapi/types/dist/core").Strapi;
|
|
30
|
+
}) => {
|
|
31
|
+
acquire(name: string, ttlMs: number): Promise<boolean>;
|
|
32
|
+
release(name: string): Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
/**
|
|
3
|
+
* A best-effort lock held in the core store.
|
|
4
|
+
*
|
|
5
|
+
* Two instances behind a load balancer run the same cron, so without this they
|
|
6
|
+
* both start pruning the same rows at the same time. Deleting a row twice is
|
|
7
|
+
* harmless; doing the whole scan twice on a large table is not.
|
|
8
|
+
*
|
|
9
|
+
* Best-effort is the honest description: read-then-write is not atomic, so two
|
|
10
|
+
* instances starting within the same millisecond can both win. That is
|
|
11
|
+
* acceptable for a nightly tidy-up and not for anything else — do not reuse
|
|
12
|
+
* this for something that must happen exactly once.
|
|
13
|
+
*/
|
|
14
|
+
declare const lock: ({ strapi }: {
|
|
15
|
+
strapi: Core.Strapi;
|
|
16
|
+
}) => {
|
|
17
|
+
acquire(name: string, ttlMs: number): Promise<boolean>;
|
|
18
|
+
release(name: string): Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
export default lock;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
import type { RelationRef } from './serializer';
|
|
3
|
+
export interface RestorePreview {
|
|
4
|
+
/** Fields that will be written back. */
|
|
5
|
+
fieldsToRestore: string[];
|
|
6
|
+
/** In the model but not in the version — left exactly as they are. */
|
|
7
|
+
fieldsKeptAsIs: string[];
|
|
8
|
+
/** In the version but no longer in the model — skipped. */
|
|
9
|
+
fieldsDropped: string[];
|
|
10
|
+
/** Not localised, so restoring them changes every locale at once. */
|
|
11
|
+
crossLocaleFields: string[];
|
|
12
|
+
/** Polymorphic relations, which this version of the plugin does not carry. */
|
|
13
|
+
unsupportedFields: string[];
|
|
14
|
+
/** Relation targets that no longer exist. */
|
|
15
|
+
brokenRelations: RelationRef[];
|
|
16
|
+
/** Media that has since been deleted. */
|
|
17
|
+
missingMedia: RelationRef[];
|
|
18
|
+
/** Locales affected by the cross-locale fields above. */
|
|
19
|
+
affectedLocales: string[];
|
|
20
|
+
}
|
|
21
|
+
declare const restore: ({ strapi }: {
|
|
22
|
+
strapi: Core.Strapi;
|
|
23
|
+
}) => {
|
|
24
|
+
/**
|
|
25
|
+
* What restoring this version would actually do.
|
|
26
|
+
*
|
|
27
|
+
* A separate call, and the UI shows it before asking for confirmation.
|
|
28
|
+
* Restore without a preview of the consequences is a trap rather than a
|
|
29
|
+
* tool — `crossLocaleFields` in particular, where restoring the Polish
|
|
30
|
+
* version of a document quietly rewrites a field in every other language.
|
|
31
|
+
*/
|
|
32
|
+
preview(versionId: number): Promise<RestorePreview>;
|
|
33
|
+
/**
|
|
34
|
+
* Writes the version back, to the draft only.
|
|
35
|
+
*
|
|
36
|
+
* The document lands in "modified" and publishing stays a deliberate act —
|
|
37
|
+
* restoring must never change what the public sees on its own.
|
|
38
|
+
*/
|
|
39
|
+
apply(versionId: number, userId: number | null): Promise<{
|
|
40
|
+
documentId: string;
|
|
41
|
+
warnings: string[];
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
export default restore;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
export interface PruneResult {
|
|
3
|
+
scanned: number;
|
|
4
|
+
deleted: number;
|
|
5
|
+
documents: number;
|
|
6
|
+
}
|
|
7
|
+
declare const retention: ({ strapi }: {
|
|
8
|
+
strapi: Core.Strapi;
|
|
9
|
+
}) => {
|
|
10
|
+
/**
|
|
11
|
+
* Thins the history according to the configured policy.
|
|
12
|
+
*
|
|
13
|
+
* Returns what it did rather than logging and forgetting, so the cron can
|
|
14
|
+
* report it and a test can assert on it.
|
|
15
|
+
*/
|
|
16
|
+
prune(now?: Date): Promise<PruneResult>;
|
|
17
|
+
};
|
|
18
|
+
export default retention;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a stored document into a version, and back.
|
|
3
|
+
*
|
|
4
|
+
* Written for this plugin rather than lifted from Strapi's own history module.
|
|
5
|
+
* Where the shapes here resemble theirs it is because Strapi's populate API
|
|
6
|
+
* dictates them — a relation can only be populated by naming its fields, a
|
|
7
|
+
* dynamic zone only through `on` fragments — not because the code was copied.
|
|
8
|
+
* Their implementation is MIT and was read while working out what the problem
|
|
9
|
+
* actually is; two facts learned that way are worth stating outright, since
|
|
10
|
+
* both cost a day if you meet them the hard way:
|
|
11
|
+
*
|
|
12
|
+
* - a component's `id` must not be stored, or restoring into a component row
|
|
13
|
+
* that has since been recreated throws;
|
|
14
|
+
* - the fields in FIELDS_TO_IGNORE change on every write, so a version that
|
|
15
|
+
* keeps them can never be compared against another one.
|
|
16
|
+
*
|
|
17
|
+
* `buildSchemaSnapshot` goes further than theirs on purpose: Strapi's descends a
|
|
18
|
+
* single level and carries an open TODO about nested components, which leaves a
|
|
19
|
+
* component inside a component indistinguishable from a field that never
|
|
20
|
+
* existed.
|
|
21
|
+
*/
|
|
22
|
+
import type { Core } from '@strapi/strapi';
|
|
23
|
+
/**
|
|
24
|
+
* Fields Strapi manages, which a version has no business storing or restoring.
|
|
25
|
+
*
|
|
26
|
+
* The timestamps and author columns change on every write, so a version that
|
|
27
|
+
* keeps them can never be compared with another. `locale` and `localizations`
|
|
28
|
+
* are injected by i18n and describe how the document is wired up rather than
|
|
29
|
+
* what it says — restoring them would try to rewrite the translation graph.
|
|
30
|
+
*/
|
|
31
|
+
export declare const FIELDS_TO_IGNORE: string[];
|
|
32
|
+
export interface RelationRef {
|
|
33
|
+
documentId?: string;
|
|
34
|
+
/** Media lives in `plugin::upload.file` and is addressed by numeric id. */
|
|
35
|
+
id?: number;
|
|
36
|
+
locale?: string | null;
|
|
37
|
+
targetUid: string;
|
|
38
|
+
}
|
|
39
|
+
export interface SplitDocument {
|
|
40
|
+
data: Record<string, unknown>;
|
|
41
|
+
relations: Record<string, RelationRef[]>;
|
|
42
|
+
schemaSnapshot: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
type Attribute = Record<string, any>;
|
|
45
|
+
/** Morph relations are skipped: Strapi's own populate builder skips them too. */
|
|
46
|
+
export declare const isMorphRelation: (attribute: Attribute) => boolean;
|
|
47
|
+
/** Populate object covering everything a snapshot needs, and nothing more. */
|
|
48
|
+
export declare const buildDeepPopulate: (strapi: Core.Strapi, uid: string, useDatabaseSyntax?: boolean) => Record<string, unknown>;
|
|
49
|
+
/**
|
|
50
|
+
* The content type's attributes plus every component reachable from it, however
|
|
51
|
+
* deeply nested.
|
|
52
|
+
*
|
|
53
|
+
* Without the nesting a restore cannot tell "this field was empty" from "this
|
|
54
|
+
* field did not exist yet" for anything below the first level of components.
|
|
55
|
+
*/
|
|
56
|
+
export declare const buildSchemaSnapshot: (strapi: Core.Strapi, uid: string) => Record<string, unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Splits a populated entry into the three things a version stores.
|
|
59
|
+
*
|
|
60
|
+
* Relations are kept apart from `data` so a diff can treat "the body changed"
|
|
61
|
+
* and "the author changed" as different kinds of change, and so a restore can
|
|
62
|
+
* check each target still exists before writing it back.
|
|
63
|
+
*/
|
|
64
|
+
export declare const split: (strapi: Core.Strapi, uid: string, entry: Record<string, unknown>) => SplitDocument;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
import type { SnapshotIntent } from '../utils/captureContext';
|
|
3
|
+
export declare const VERSION_UID = "plugin::rewind.version";
|
|
4
|
+
declare const snapshot: ({ strapi }: {
|
|
5
|
+
strapi: Core.Strapi;
|
|
6
|
+
}) => {
|
|
7
|
+
readRows: (uid: string, relatedDocumentId: string, locales: (string | null)[]) => Promise<Record<string, unknown>[]>;
|
|
8
|
+
/**
|
|
9
|
+
* Writes one version per affected locale, for each buffered intent.
|
|
10
|
+
*
|
|
11
|
+
* Called from the transaction's commit hook and never awaited by the write
|
|
12
|
+
* path — a failure here must not be able to reach the editor's save.
|
|
13
|
+
*/
|
|
14
|
+
captureAll(intents: SnapshotIntent[], userId: number | null): Promise<void>;
|
|
15
|
+
capture(intent: SnapshotIntent, userId: number | null): Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
export default snapshot;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Core } from '@strapi/strapi';
|
|
2
|
+
/**
|
|
3
|
+
* Gives versions recorded before labels existed something to show.
|
|
4
|
+
*
|
|
5
|
+
* Without this, upgrading leaves a history where older entries are a badge and
|
|
6
|
+
* a timestamp and newer ones have a title — the list stays half-unreadable for
|
|
7
|
+
* exactly as long as the old versions are worth keeping.
|
|
8
|
+
*
|
|
9
|
+
* Two details that matter:
|
|
10
|
+
*
|
|
11
|
+
* - Rows whose content yields no label are written as an empty string, not left
|
|
12
|
+
* null. Leaving them null would make the next batch select the same rows
|
|
13
|
+
* again, forever.
|
|
14
|
+
* - It runs detached. The work is proportional to how many versions exist, and
|
|
15
|
+
* a site with a long history should not pay minutes of boot time for a
|
|
16
|
+
* cosmetic column.
|
|
17
|
+
*/
|
|
18
|
+
export declare const backfillLabels: (strapi: Core.Strapi, versionUid: string) => Promise<number>;
|