strapi-plugin-document-metadata 1.0.1

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 (35) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +67 -0
  3. package/dist/_chunks/en-h9IAaoUJ.mjs +16 -0
  4. package/dist/_chunks/en-tDvQkVOI.js +16 -0
  5. package/dist/admin/index.js +311 -0
  6. package/dist/admin/index.mjs +312 -0
  7. package/dist/admin/src/components/DocumentMetadataCard/index.d.ts +10 -0
  8. package/dist/admin/src/components/DocumentMetadataGuard/index.d.ts +6 -0
  9. package/dist/admin/src/components/Initializer/index.d.ts +5 -0
  10. package/dist/admin/src/components/LastOpenedMetadataGuard/index.d.ts +10 -0
  11. package/dist/admin/src/components/LastOpenedMetadataLoader/index.d.ts +10 -0
  12. package/dist/admin/src/components/LastOpenedMetadataLoader/useLastOpened.d.ts +31 -0
  13. package/dist/admin/src/components/MetadataRow/index.d.ts +9 -0
  14. package/dist/admin/src/index.d.ts +10 -0
  15. package/dist/admin/src/pluginId.d.ts +1 -0
  16. package/dist/admin/src/utils/prefixKey.d.ts +2 -0
  17. package/dist/admin/src/utils/recentTimeFormatter.d.ts +16 -0
  18. package/dist/admin/src/utils/relativeDateFormatter.d.ts +30 -0
  19. package/dist/server/index.js +119 -0
  20. package/dist/server/index.mjs +120 -0
  21. package/dist/server/src/bootstrap.d.ts +5 -0
  22. package/dist/server/src/config/index.d.ts +5 -0
  23. package/dist/server/src/content-types/index.d.ts +2 -0
  24. package/dist/server/src/controllers/controller.d.ts +12 -0
  25. package/dist/server/src/controllers/index.d.ts +9 -0
  26. package/dist/server/src/destroy.d.ts +5 -0
  27. package/dist/server/src/index.d.ts +58 -0
  28. package/dist/server/src/middlewares/index.d.ts +2 -0
  29. package/dist/server/src/policies/index.d.ts +2 -0
  30. package/dist/server/src/register.d.ts +5 -0
  31. package/dist/server/src/routes/admin.d.ts +12 -0
  32. package/dist/server/src/routes/index.d.ts +14 -0
  33. package/dist/server/src/services/index.d.ts +19 -0
  34. package/dist/server/src/services/service.d.ts +35 -0
  35. package/package.json +78 -0
@@ -0,0 +1,312 @@
1
+ import { useRef, useEffect, useState } from "react";
2
+ import { jsxs, jsx } from "react/jsx-runtime";
3
+ import { useLocation } from "react-router-dom";
4
+ import { useFetchClient, useQueryParams, unstable_useDocument } from "@strapi/strapi/admin";
5
+ import { Grid, Typography, Box, Flex, Divider } from "@strapi/design-system";
6
+ import { Paperclip } from "@strapi/icons";
7
+ import { useIntl, FormattedMessage } from "react-intl";
8
+ const __variableDynamicImportRuntimeHelper = (glob, path, segs) => {
9
+ const v = glob[path];
10
+ if (v) {
11
+ return typeof v === "function" ? v() : Promise.resolve(v);
12
+ }
13
+ return new Promise((_, reject) => {
14
+ (typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)(
15
+ reject.bind(
16
+ null,
17
+ new Error(
18
+ "Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "")
19
+ )
20
+ )
21
+ );
22
+ });
23
+ };
24
+ const PLUGIN_ID = "document-metadata";
25
+ const Initializer = ({ setPlugin }) => {
26
+ const ref = useRef(setPlugin);
27
+ useEffect(() => {
28
+ ref.current(PLUGIN_ID);
29
+ }, []);
30
+ return null;
31
+ };
32
+ const prefixKey = (key) => `${PLUGIN_ID}.${key}`;
33
+ const relativeDateFormatter = (date, textBuilder) => {
34
+ const today = /* @__PURE__ */ new Date();
35
+ if (today.toDateString() === date.toDateString()) {
36
+ return textBuilder.today(date.toLocaleTimeString());
37
+ }
38
+ const yesterday = /* @__PURE__ */ new Date();
39
+ yesterday.setDate(yesterday.getDate() - 1);
40
+ if (yesterday.toDateString() === date.toDateString()) {
41
+ return textBuilder.yesterday(date.toLocaleTimeString());
42
+ }
43
+ return textBuilder.other(date.toLocaleString());
44
+ };
45
+ const config$3 = {
46
+ /** The threshold in minutes to determine if a date is considered "recent". */
47
+ thresholdInMinutes: 60
48
+ };
49
+ const recentTimeFormatter = ({
50
+ date,
51
+ fallbackFormatter
52
+ }) => {
53
+ const now = /* @__PURE__ */ new Date();
54
+ const minutesElapsedSinceNow = minutesElapsed(now, date);
55
+ if (minutesElapsedSinceNow >= config$3.thresholdInMinutes) {
56
+ return fallbackFormatter(date);
57
+ }
58
+ const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
59
+ return rtf.format(-minutesElapsedSinceNow, "minutes");
60
+ };
61
+ const minutesElapsed = (now, then) => {
62
+ const millisecondsPerMinute = 60 * 1e3;
63
+ return Math.floor((now.getTime() - then.getTime()) / millisecondsPerMinute);
64
+ };
65
+ const MetadataRow = ({ title, line1, line2 }) => {
66
+ return /* @__PURE__ */ jsxs(Grid.Item, { direction: "column", alignItems: "stretch", children: [
67
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: title }),
68
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
69
+ line1,
70
+ /* @__PURE__ */ jsx("br", {}),
71
+ line2
72
+ ] })
73
+ ] });
74
+ };
75
+ var FetchStatus = /* @__PURE__ */ ((FetchStatus2) => {
76
+ FetchStatus2["Initial"] = "initial";
77
+ FetchStatus2["InProgress"] = "in-progress";
78
+ FetchStatus2["Success"] = "success";
79
+ FetchStatus2["Failure"] = "failure";
80
+ return FetchStatus2;
81
+ })(FetchStatus || {});
82
+ const config$2 = {
83
+ /** The configuration for the last-opened request. */
84
+ lastOpenedRequest: {
85
+ /** The path to fetch the last-opened metadata for the entity with the given `uid` and `documentId`. */
86
+ path: (uid, documentId) => `/document-metadata/last-opened/${uid}/${documentId}`
87
+ }
88
+ };
89
+ const useLastOpened = ({
90
+ uid,
91
+ documentId,
92
+ locale
93
+ }) => {
94
+ const fetchClient = useFetchClient();
95
+ const [lastOpenedFetchState, setLastOpenedFetchState] = useState({
96
+ status: "initial"
97
+ /* Initial */
98
+ });
99
+ useEffect(() => {
100
+ const fetchLastOpened = async () => {
101
+ setLastOpenedFetchState({
102
+ status: "in-progress"
103
+ /* InProgress */
104
+ });
105
+ try {
106
+ const { data: lastOpened } = await fetchClient.get(
107
+ config$2.lastOpenedRequest.path(uid, documentId),
108
+ {
109
+ params: { locale }
110
+ }
111
+ );
112
+ setLastOpenedFetchState({ status: "success", lastOpened });
113
+ } catch (error) {
114
+ console.error(`Failed to fetch last-opened metadata: ${error}`);
115
+ setLastOpenedFetchState({ status: "failure", error });
116
+ }
117
+ };
118
+ fetchLastOpened();
119
+ }, [uid, documentId, locale]);
120
+ return lastOpenedFetchState;
121
+ };
122
+ const config$1 = {
123
+ /** The loading text to show while fetching data. */
124
+ loadingString: "…",
125
+ /** The non-breaking space character. */
126
+ nonBreakingSpace: " "
127
+ };
128
+ const LastOpenedMetadataLoader = ({
129
+ uid,
130
+ documentId,
131
+ locale
132
+ }) => {
133
+ const { formatMessage } = useIntl();
134
+ const translate = (key, values) => formatMessage({ id: prefixKey(key) }, values);
135
+ const lastOpenedFetchState = useLastOpened({ uid, documentId, locale });
136
+ switch (lastOpenedFetchState.status) {
137
+ case FetchStatus.Initial:
138
+ case FetchStatus.InProgress:
139
+ return /* @__PURE__ */ jsx(
140
+ MetadataRow,
141
+ {
142
+ title: translate("opened-at"),
143
+ line1: config$1.loadingString,
144
+ line2: config$1.nonBreakingSpace
145
+ }
146
+ );
147
+ case FetchStatus.Failure:
148
+ return null;
149
+ case FetchStatus.Success:
150
+ const lastOpened = lastOpenedFetchState.lastOpened;
151
+ if (!lastOpened.openedAt || !lastOpened.openedBy) {
152
+ return /* @__PURE__ */ jsx(MetadataRow, { title: translate("opened-at"), line1: translate("opened-first-time") });
153
+ }
154
+ const formattedOpenedAt = recentTimeFormatter({
155
+ date: new Date(lastOpened.openedAt),
156
+ fallbackFormatter: (date) => relativeDateFormatter(date, {
157
+ today: (formattedTime) => translate("date.today", { formattedTime }),
158
+ yesterday: (formattedTime) => translate("date.yesterday", { formattedTime }),
159
+ other: (formattedDate) => translate("date.other", { formattedDate })
160
+ })
161
+ });
162
+ const formattedOpenedBy = translate("opened-by", { username: lastOpened.openedBy });
163
+ return /* @__PURE__ */ jsx(
164
+ MetadataRow,
165
+ {
166
+ title: translate("opened-at"),
167
+ line1: formattedOpenedAt,
168
+ line2: formattedOpenedBy
169
+ }
170
+ );
171
+ }
172
+ };
173
+ const LastOpenedMetadataGuard = ({
174
+ uid,
175
+ document
176
+ }) => {
177
+ const hasLastOpenedFields = "openedAt" in document && "openedBy" in document;
178
+ if (!hasLastOpenedFields) {
179
+ return null;
180
+ }
181
+ return /* @__PURE__ */ jsx(LastOpenedMetadataLoader, { uid, documentId: document.documentId, locale: document.locale });
182
+ };
183
+ const DocumentMetadataCard = ({
184
+ collectionType,
185
+ uid,
186
+ documentId
187
+ }) => {
188
+ const { formatMessage } = useIntl();
189
+ const translate = (key, values) => formatMessage({ id: prefixKey(key) }, values);
190
+ const initialParams = { plugins: { i18n: { locale: void 0 } } };
191
+ const [queryParams, _] = useQueryParams(initialParams);
192
+ const locale = queryParams.query.plugins.i18n.locale;
193
+ const { document } = unstable_useDocument({ documentId, model: uid, collectionType, params: { locale } });
194
+ if (!document) {
195
+ return null;
196
+ }
197
+ const formatDate = (date) => recentTimeFormatter({
198
+ date: new Date(date),
199
+ fallbackFormatter: (date2) => relativeDateFormatter(date2, {
200
+ today: (formattedTime) => translate("date.today", { formattedTime }),
201
+ yesterday: (formattedTime) => translate("date.yesterday", { formattedTime }),
202
+ other: (formattedDate) => translate("date.other", { formattedDate })
203
+ })
204
+ });
205
+ const formatUsername = (user) => `${user.firstname} ${user.lastname}`;
206
+ let formattedUpdatedAt = formatDate(new Date(document.updatedAt));
207
+ let formattedUpdatedBy = document.updatedBy ? translate("updated-by", { username: formatUsername(document.updatedBy) }) : "";
208
+ let formattedCreatedAt = formatDate(new Date(document.createdAt));
209
+ let formattedCreatedBy = document.createdBy ? translate("created-by", { username: formatUsername(document.createdBy) }) : "";
210
+ return /* @__PURE__ */ jsx(
211
+ Box,
212
+ {
213
+ width: "100%",
214
+ padding: "16px",
215
+ background: "neutral0",
216
+ hasRadius: true,
217
+ borderColor: "neutral200",
218
+ borderStyle: "solid",
219
+ borderWidth: "1px",
220
+ children: /* @__PURE__ */ jsxs(Grid.Root, { gap: "8px", gridCols: 1, children: [
221
+ /* @__PURE__ */ jsxs(Grid.Item, { direction: "column", alignItems: "stretch", children: [
222
+ /* @__PURE__ */ jsxs(Flex, { gap: "8px", direction: "row", alignItems: "center", justifyContent: "space-between", children: [
223
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: /* @__PURE__ */ jsx(FormattedMessage, { id: prefixKey("title") }) }),
224
+ /* @__PURE__ */ jsx(Paperclip, { fill: "neutral600", style: { marginRight: "4px" } })
225
+ ] }),
226
+ /* @__PURE__ */ jsx(Divider, { style: { marginTop: "6px", marginBottom: "4px" } })
227
+ ] }),
228
+ /* @__PURE__ */ jsx(LastOpenedMetadataGuard, { uid, document }),
229
+ /* @__PURE__ */ jsx(
230
+ MetadataRow,
231
+ {
232
+ title: translate("updated-at"),
233
+ line1: formattedUpdatedAt,
234
+ line2: formattedUpdatedBy
235
+ }
236
+ ),
237
+ /* @__PURE__ */ jsx(
238
+ MetadataRow,
239
+ {
240
+ title: translate("created-at"),
241
+ line1: formattedCreatedAt,
242
+ line2: formattedCreatedBy
243
+ }
244
+ )
245
+ ] })
246
+ }
247
+ );
248
+ };
249
+ const config = {
250
+ /**
251
+ * The supported collection type.
252
+ * We currently only support "collection-types", as "single-types" don't have their document ID as part of the URL.
253
+ */
254
+ supportedCollectionType: "collection-types",
255
+ /**
256
+ * The expected length of a valid document ID.
257
+ *
258
+ * > To address this limitation, Strapi 5 introduced documentId, a 24-character alphanumeric string, as a unique and
259
+ * > persistent identifier for a content entry, independent of its physical records.
260
+ *
261
+ * https://docs.strapi.io/cms/api/document-service
262
+ */
263
+ documentIdLength: 24
264
+ };
265
+ const DocumentMetadataGuard = () => {
266
+ const location = useLocation();
267
+ const urlPathComponents = location.pathname.split("/").filter(Boolean);
268
+ const [collectionType, uid, documentId] = urlPathComponents.slice(-3);
269
+ const isValidCollectionType = collectionType === config.supportedCollectionType;
270
+ const isValidUID = uid && uid.length;
271
+ const isValidDocumentId = documentId && documentId.length === config.documentIdLength;
272
+ if (!isValidCollectionType || !isValidUID || !isValidDocumentId) {
273
+ return null;
274
+ }
275
+ return /* @__PURE__ */ jsx(
276
+ DocumentMetadataCard,
277
+ {
278
+ collectionType,
279
+ uid,
280
+ documentId
281
+ }
282
+ );
283
+ };
284
+ const index = {
285
+ register(app) {
286
+ app.getPlugin("content-manager").injectComponent("editView", "right-links", {
287
+ name: "DocumentMetadataGuard",
288
+ Component: DocumentMetadataGuard
289
+ });
290
+ app.registerPlugin({
291
+ id: PLUGIN_ID,
292
+ initializer: Initializer,
293
+ isReady: false,
294
+ name: PLUGIN_ID
295
+ });
296
+ },
297
+ async registerTrads({ locales }) {
298
+ return Promise.all(
299
+ locales.map(async (locale) => {
300
+ try {
301
+ const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("../_chunks/en-h9IAaoUJ.mjs") }), `./translations/${locale}.json`, 3);
302
+ return { data, locale };
303
+ } catch {
304
+ return { data: {}, locale };
305
+ }
306
+ })
307
+ );
308
+ }
309
+ };
310
+ export {
311
+ index as default
312
+ };
@@ -0,0 +1,10 @@
1
+ import type { CollectionType, ContentTypeUID, DocumentID } from '../../types';
2
+ /**
3
+ * Renders a document metadata card showing various metadata about the given document.
4
+ */
5
+ declare const DocumentMetadataCard: ({ collectionType, uid, documentId, }: {
6
+ collectionType: CollectionType;
7
+ uid: ContentTypeUID;
8
+ documentId: DocumentID;
9
+ }) => import("react/jsx-runtime").JSX.Element | null;
10
+ export default DocumentMetadataCard;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * A wrapper component that conditionally renders the `DocumentMetadataCard` and its contents
3
+ * when the current document being edited is supported.
4
+ */
5
+ declare const DocumentMetadataGuard: () => import("react/jsx-runtime").JSX.Element | null;
6
+ export default DocumentMetadataGuard;
@@ -0,0 +1,5 @@
1
+ type InitializerProps = {
2
+ setPlugin: (id: string) => void;
3
+ };
4
+ declare const Initializer: ({ setPlugin }: InitializerProps) => null;
5
+ export { Initializer };
@@ -0,0 +1,10 @@
1
+ import type { AnyDocument, ContentTypeUID } from '../../types';
2
+ /**
3
+ * A wrapper component that conditionally renders the `LastOpenedMetadataLoader`
4
+ * when the current content-type being edited contains the last-opened fields.
5
+ */
6
+ declare const LastOpenedMetadataGuard: ({ uid, document, }: {
7
+ uid: ContentTypeUID;
8
+ document: AnyDocument;
9
+ }) => import("react/jsx-runtime").JSX.Element | null;
10
+ export default LastOpenedMetadataGuard;
@@ -0,0 +1,10 @@
1
+ import type { ContentTypeUID, DocumentID } from '../../types';
2
+ /**
3
+ * Fetches the last-opened metadata for the given document.
4
+ */
5
+ declare const LastOpenedMetadataLoader: ({ uid, documentId, locale, }: {
6
+ uid: ContentTypeUID;
7
+ documentId: DocumentID;
8
+ locale: string | undefined;
9
+ }) => import("react/jsx-runtime").JSX.Element | null;
10
+ export default LastOpenedMetadataLoader;
@@ -0,0 +1,31 @@
1
+ import type { ContentTypeUID, DocumentID, LastOpened } from '../../types';
2
+ /**
3
+ * Represents the different states of a fetch workflow.
4
+ */
5
+ export declare enum FetchStatus {
6
+ Initial = "initial",
7
+ InProgress = "in-progress",
8
+ Success = "success",
9
+ Failure = "failure"
10
+ }
11
+ /**
12
+ * A discriminated union describing the current state of last-opened fetch process.
13
+ * Each state can optionally include payload data relevant to that stage.
14
+ */
15
+ type LastOpenedFetchState = {
16
+ status: FetchStatus.Initial;
17
+ } | {
18
+ status: FetchStatus.InProgress;
19
+ } | {
20
+ status: FetchStatus.Success;
21
+ lastOpened: LastOpened;
22
+ } | {
23
+ status: FetchStatus.Failure;
24
+ error: any;
25
+ };
26
+ export declare const useLastOpened: ({ uid, documentId, locale, }: {
27
+ uid: ContentTypeUID;
28
+ documentId: DocumentID;
29
+ locale: string | undefined;
30
+ }) => LastOpenedFetchState;
31
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Displays a single metadata row consisting of a title and one or two lines of descriptive text.
3
+ */
4
+ declare const MetadataRow: ({ title, line1, line2 }: {
5
+ title: string;
6
+ line1: string;
7
+ line2?: string;
8
+ }) => import("react/jsx-runtime").JSX.Element;
9
+ export default MetadataRow;
@@ -0,0 +1,10 @@
1
+ declare const _default: {
2
+ register(app: any): void;
3
+ registerTrads({ locales }: {
4
+ locales: string[];
5
+ }): Promise<{
6
+ data: any;
7
+ locale: string;
8
+ }[]>;
9
+ };
10
+ export default _default;
@@ -0,0 +1 @@
1
+ export declare const PLUGIN_ID = "document-metadata";
@@ -0,0 +1,2 @@
1
+ declare const prefixKey: (key: string) => string;
2
+ export { prefixKey };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Formats a given `Date` object into a human-readable string for **recent dates**,
3
+ * and delegates to a fallback formatter for older dates.
4
+ *
5
+ * - If the date is within the **last 60 minutes**, it returns a relative time string like `"5 minutes ago"`.
6
+ * - Otherwise, it falls back to the `relativeDateFormatter()` function for formatting.
7
+ *
8
+ * @param params.date - The `Date` object to format.
9
+ * @param params.fallbackFormatter - A function that formats dates which are not considered "recent".
10
+ *
11
+ * @returns A human-readable relative date string based on the provided date.
12
+ */
13
+ export declare const recentTimeFormatter: ({ date, fallbackFormatter, }: {
14
+ date: Date;
15
+ fallbackFormatter: (date: Date) => string;
16
+ }) => string;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Defines a set of functions that build human-readable text for relative dates.
3
+ *
4
+ * This interface allows customization of how "today", "yesterday",
5
+ * and "other" dates are represented in formatted output.
6
+ *
7
+ * Example:
8
+ * ```ts
9
+ * const textBuilder: RelativeDateTextBuilder = {
10
+ * today: (time) => `Today at ${time}`,
11
+ * yesterday: (time) => `Yesterday at ${time}`,
12
+ * other: (date) => date,
13
+ * };
14
+ * ```
15
+ */
16
+ interface RelativeDateTextBuilder {
17
+ today: (formattedTime: string) => string;
18
+ yesterday: (formattedTime: string) => string;
19
+ other: (formattedDate: string) => string;
20
+ }
21
+ /**
22
+ * Formats a given `Date` object into a human-readable, relative date string.
23
+ *
24
+ * @param date - The `Date` object to format.
25
+ * @param textBuilder - A builder object that defines how to format "today", "yesterday", and other dates.
26
+ *
27
+ * @returns A human-readable relative date string based on the provided date.
28
+ */
29
+ export declare const relativeDateFormatter: (date: Date, textBuilder: RelativeDateTextBuilder) => string;
30
+ export {};
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ const bootstrap = ({ strapi }) => {
3
+ };
4
+ const destroy = ({ strapi }) => {
5
+ };
6
+ const register = ({ strapi }) => {
7
+ };
8
+ const config = {
9
+ default: {},
10
+ validator() {
11
+ }
12
+ };
13
+ const contentTypes = {};
14
+ const controller = ({ strapi }) => ({
15
+ /**
16
+ * Controller method for the route that fetches and updates the last-opened fields
17
+ * of a document with the given `uid` and `documentId` path parameters (GET request).
18
+ */
19
+ async lastOpened(ctx) {
20
+ const { uid, documentId } = ctx.params;
21
+ const { locale } = ctx.request.query;
22
+ const previousLastOpened = await strapi.plugin("document-metadata").service("service").fetchLastOpened({ uid, documentId, locale });
23
+ const openedAt = (/* @__PURE__ */ new Date()).toISOString();
24
+ const { user } = ctx.state;
25
+ const openedBy = `${user.firstname} ${user.lastname}`;
26
+ await strapi.plugin("document-metadata").service("service").updateLastOpened({ uid, documentId, locale, openedAt, openedBy });
27
+ ctx.response.body = previousLastOpened;
28
+ }
29
+ });
30
+ const controllers = {
31
+ controller
32
+ };
33
+ const middlewares = {};
34
+ const policies = {};
35
+ const admin = {
36
+ type: "admin",
37
+ routes: [
38
+ {
39
+ method: "GET",
40
+ path: "/last-opened/:uid/:documentId",
41
+ handler: "controller.lastOpened",
42
+ config: {
43
+ policies: []
44
+ }
45
+ }
46
+ ]
47
+ };
48
+ const routes = {
49
+ admin
50
+ };
51
+ const service = ({ strapi }) => ({
52
+ /**
53
+ * Fetches the last-opened fields for a specific document within a content type.
54
+ *
55
+ * @param uid - The unique identifier of the content type (e.g. 'api::products.products').
56
+ * @param documentId - The ID of the document to fetch.
57
+ * @param locale - The current locale of the content type / `undefined` if localization is turned off.
58
+ */
59
+ fetchLastOpened: async ({
60
+ uid,
61
+ documentId,
62
+ locale
63
+ }) => {
64
+ return await strapi.documents(uid).findOne({
65
+ documentId,
66
+ fields: ["openedAt", "openedBy"],
67
+ locale
68
+ });
69
+ },
70
+ /**
71
+ * Updates the last-opened fields for a specific document within a content type.
72
+ *
73
+ * @param uid - The unique identifier of the content type (e.g. 'api::products.products').
74
+ * @param documentId - The ID of the document to update.
75
+ * @param locale - The current locale of the content type / `undefined` if localization is turned off.
76
+ * @param openedAt - The date and time when the document was last opened.
77
+ * @param openedBy - The name of the user who last opened the document.
78
+ */
79
+ async updateLastOpened({
80
+ uid,
81
+ documentId,
82
+ locale,
83
+ openedAt,
84
+ openedBy
85
+ }) {
86
+ const tableName = strapi.getModel(uid).collectionName;
87
+ if (!tableName) {
88
+ throw new Error(
89
+ `Expected to have a collection name for the content type "${uid}" at this point.`
90
+ );
91
+ }
92
+ return await strapi.db.connection(tableName).update({
93
+ opened_at: openedAt,
94
+ opened_by: openedBy
95
+ }).where({
96
+ document_id: documentId,
97
+ // We explicitly need to provide `null` here, cause in the database
98
+ // the locale is stored as `NULL` when localization is turned off.
99
+ // Without this fallback, the query would not match any rows.
100
+ locale: locale || null
101
+ });
102
+ }
103
+ });
104
+ const services = {
105
+ service
106
+ };
107
+ const index = {
108
+ register,
109
+ bootstrap,
110
+ destroy,
111
+ config,
112
+ controllers,
113
+ routes,
114
+ services,
115
+ contentTypes,
116
+ policies,
117
+ middlewares
118
+ };
119
+ module.exports = index;