@directus/api 35.2.0 → 36.0.0-rc.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.
- package/dist/ai/chat/models/chat-request.js +48 -48
- package/dist/ai/chat/models/object-request.js +6 -6
- package/dist/ai/chat/models/providers.js +14 -14
- package/dist/ai/chat/utils/parse-json-schema-7.js +22 -22
- package/dist/ai/mcp/server.js +44 -6
- package/dist/ai/mcp/utils.js +31 -0
- package/dist/ai/tools/assets/index.js +3 -3
- package/dist/ai/tools/collections/index.js +18 -18
- package/dist/ai/tools/fields/index.js +18 -18
- package/dist/ai/tools/files/index.js +18 -18
- package/dist/ai/tools/flows/index.js +16 -16
- package/dist/ai/tools/folders/index.js +18 -18
- package/dist/ai/tools/items/index.js +17 -17
- package/dist/ai/tools/operations/index.js +16 -16
- package/dist/ai/tools/relations/index.js +22 -22
- package/dist/ai/tools/schema/index.js +3 -3
- package/dist/ai/tools/schema.js +159 -159
- package/dist/ai/tools/system/index.js +3 -3
- package/dist/ai/tools/trigger-flow/index.js +3 -3
- package/dist/app.js +35 -11
- package/dist/auth/drivers/ldap.js +3 -1
- package/dist/auth/drivers/local.js +2 -0
- package/dist/auth/drivers/oauth2.js +3 -1
- package/dist/auth/drivers/openid.js +3 -1
- package/dist/auth/drivers/saml.js +2 -0
- package/dist/auth/utils/check-local-disabled.js +16 -0
- package/dist/auth/utils/check-sso-enabled.js +14 -0
- package/dist/auth.js +8 -5
- package/dist/cli/commands/bootstrap/index.js +3 -0
- package/dist/cli/commands/cache/clear.js +6 -1
- package/dist/cli/commands/roles/create.js +4 -1
- package/dist/cli/commands/users/create.js +3 -0
- package/dist/constants.js +8 -1
- package/dist/controllers/access.js +1 -1
- package/dist/controllers/activity.js +2 -1
- package/dist/controllers/assets.js +2 -0
- package/dist/controllers/auth.js +13 -5
- package/dist/controllers/collections.js +1 -1
- package/dist/controllers/comments.js +1 -1
- package/dist/controllers/dashboards.js +1 -1
- package/dist/controllers/fields.js +1 -1
- package/dist/controllers/files.js +3 -1
- package/dist/controllers/flows.js +6 -5
- package/dist/controllers/folders.js +1 -1
- package/dist/controllers/graphql.js +2 -0
- package/dist/controllers/items.js +3 -1
- package/dist/controllers/license.js +119 -0
- package/dist/controllers/mcp/index.js +38 -0
- package/dist/controllers/mcp/oauth-clients.js +68 -0
- package/dist/controllers/mcp/oauth-consent-page.js +316 -0
- package/dist/controllers/mcp/oauth.js +381 -0
- package/dist/controllers/mcp/templates/oauth-consent.liquid +62 -0
- package/dist/controllers/mcp/templates/oauth-error.liquid +28 -0
- package/dist/controllers/notifications.js +1 -1
- package/dist/controllers/operations.js +1 -1
- package/dist/controllers/panels.js +1 -1
- package/dist/controllers/permissions.js +1 -1
- package/dist/controllers/policies.js +1 -1
- package/dist/controllers/presets.js +1 -1
- package/dist/controllers/revisions.js +3 -2
- package/dist/controllers/roles.js +1 -1
- package/dist/controllers/server.js +38 -10
- package/dist/controllers/shares.js +1 -1
- package/dist/controllers/translations.js +1 -1
- package/dist/controllers/users.js +1 -1
- package/dist/controllers/utils.js +2 -2
- package/dist/controllers/versions.js +12 -5
- package/dist/database/get-ast-from-query/lib/convert-wildcards.js +10 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -1
- package/dist/database/helpers/fn/dialects/mysql.js +7 -12
- package/dist/database/helpers/fn/dialects/oracle.js +3 -4
- package/dist/database/helpers/fn/dialects/postgres.js +4 -26
- package/dist/database/helpers/fn/json/mysql-json-path.js +22 -0
- package/dist/database/helpers/fn/json/parse-function.js +14 -6
- package/dist/database/helpers/fn/json/postgres-json-path.js +54 -0
- package/dist/database/migrations/20260110A-add-ai-provider-settings.js +4 -4
- package/dist/database/migrations/20260217A-null-item-versions.js +14 -0
- package/dist/database/migrations/20260312A-add-ai-translation-settings.js +18 -0
- package/dist/database/migrations/20260507A-add-licensing.js +22 -0
- package/dist/database/migrations/20260512A-add-autosave-revision-interval.js +14 -0
- package/dist/database/migrations/20260512B-add-mcp-oauth.js +87 -0
- package/dist/database/run-ast/lib/apply-query/filter/operator.js +116 -33
- package/dist/database/run-ast/lib/apply-query/index.js +4 -1
- package/dist/database/run-ast/lib/apply-query/sort.js +17 -7
- package/dist/database/run-ast/lib/get-db-query.js +21 -9
- package/dist/database/run-ast/lib/parse-current-level.js +2 -1
- package/dist/database/run-ast/run-ast.js +2 -1
- package/dist/database/run-ast/utils/get-column.js +2 -1
- package/dist/database/run-ast/utils/merge-with-parent-items.js +5 -3
- package/dist/extensions/lib/installation/manager.js +1 -1
- package/dist/extensions/lib/sandbox/register/operation.js +1 -1
- package/dist/extensions/lib/sync/sync.js +1 -1
- package/dist/extensions/manager.js +3 -3
- package/dist/flows.js +5 -5
- package/dist/license/entitlements/lib/collections.js +37 -0
- package/dist/license/entitlements/lib/custom-llms-enabled.js +18 -0
- package/dist/license/entitlements/lib/custom-permission-rules-enabled.js +41 -0
- package/dist/license/entitlements/lib/flows.js +29 -0
- package/dist/license/entitlements/lib/seats.js +103 -0
- package/dist/license/entitlements/lib/sso-enabled.js +45 -0
- package/dist/license/entitlements/manager.js +256 -0
- package/dist/license/index.js +4 -0
- package/dist/license/manager.js +505 -0
- package/dist/license/utils/compute-license-status.js +27 -0
- package/dist/license/utils/get-core-grace-expires-at.js +38 -0
- package/dist/license/utils/get-license-key.js +23 -0
- package/dist/license/utils/get-license-token.js +23 -0
- package/dist/license/utils/handle-license-error.js +41 -0
- package/dist/license/utils/is-in-core-grace-period.js +11 -0
- package/dist/license/utils/is-sso-bypass-allowed.js +21 -0
- package/dist/license/utils/use-rpc.js +33 -0
- package/dist/middleware/cache.js +4 -1
- package/dist/middleware/error-handler.js +11 -0
- package/dist/middleware/extract-token.js +11 -2
- package/dist/middleware/is-admin.js +16 -0
- package/dist/middleware/is-locked.js +16 -0
- package/dist/middleware/mcp-oauth-guard.js +23 -0
- package/dist/middleware/request-counter.js +5 -2
- package/dist/packages/types/dist/index.js +117 -122
- package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +10 -1
- package/dist/permissions/utils/get-unaliased-field-key.js +2 -1
- package/dist/request/is-denied-ip.js +2 -0
- package/dist/schedules/license.js +31 -0
- package/dist/schedules/oauth-cleanup.js +26 -0
- package/dist/schedules/retention.js +1 -1
- package/dist/schedules/telemetry.js +4 -1
- package/dist/schedules/tus.js +1 -1
- package/dist/schedules/utils/duration-to-cron.js +36 -0
- package/dist/services/activity.js +15 -0
- package/dist/services/authentication.js +12 -5
- package/dist/services/collections.js +40 -10
- package/dist/services/fields.js +6 -6
- package/dist/services/flows.js +12 -0
- package/dist/services/graphql/resolvers/system-admin.js +2 -2
- package/dist/services/graphql/resolvers/system-global.js +1 -1
- package/dist/services/graphql/resolvers/system.js +43 -27
- package/dist/services/graphql/schema/get-types.js +28 -7
- package/dist/services/graphql/schema/parse-query.js +8 -0
- package/dist/services/graphql/schema/read.js +12 -0
- package/dist/services/graphql/types/json-filter.js +30 -0
- package/dist/services/index.js +6 -6
- package/dist/services/items.js +32 -14
- package/dist/services/mcp-oauth/cimd.js +307 -0
- package/dist/services/mcp-oauth/index.js +1185 -0
- package/dist/services/mcp-oauth/types/error.js +22 -0
- package/dist/services/mcp-oauth/utils/cimd-egress.js +182 -0
- package/dist/services/mcp-oauth/utils/domain.js +21 -0
- package/dist/services/mcp-oauth/utils/loopback.js +11 -0
- package/dist/services/mcp-oauth/utils/redirect.js +84 -0
- package/dist/services/mcp-oauth/utils/registration-debug.js +131 -0
- package/dist/services/payload.js +2 -1
- package/dist/services/permissions.js +31 -9
- package/dist/services/revisions.js +15 -0
- package/dist/services/server.js +66 -68
- package/dist/services/settings.js +37 -3
- package/dist/services/users.js +23 -6
- package/dist/services/utils.js +6 -1
- package/dist/services/versions.js +160 -70
- package/dist/utils/calculate-field-depth.js +1 -0
- package/dist/utils/create-admin.js +3 -3
- package/dist/utils/deep-freeze.js +24 -0
- package/dist/utils/extract-function-name.js +13 -0
- package/dist/utils/generate-translations.js +5 -5
- package/dist/utils/get-accountability-for-token.js +13 -1
- package/dist/utils/get-cache-key.js +1 -1
- package/dist/utils/get-history-filter-query.js +22 -0
- package/dist/utils/get-schema.js +2 -2
- package/dist/utils/get-service.js +3 -3
- package/dist/utils/is-admin.js +9 -0
- package/dist/utils/is-unauthenticated.js +15 -0
- package/dist/utils/parse-oauth-scope.js +12 -0
- package/dist/utils/sanitize-query.js +2 -2
- package/dist/utils/split-field-path.js +29 -0
- package/dist/utils/store.js +1 -1
- package/dist/utils/transaction.js +2 -2
- package/dist/utils/translations-validation.js +2 -2
- package/dist/utils/validate-query.js +35 -4
- package/dist/utils/validate-user-count-integrity.js +28 -5
- package/dist/utils/verify-session-jwt.js +5 -2
- package/dist/utils/versioning/handle-version.js +131 -48
- package/dist/utils/versioning/remove-circular.js +17 -0
- package/dist/websocket/authenticate.js +2 -1
- package/dist/websocket/collab/collab.js +1 -1
- package/dist/websocket/collab/room.js +1 -1
- package/dist/websocket/controllers/base.js +12 -0
- package/dist/websocket/controllers/graphql.js +1 -1
- package/dist/websocket/handlers/subscribe.js +1 -1
- package/dist/websocket/messages.js +64 -64
- package/dist/websocket/utils/items.js +2 -2
- package/license +90 -80
- package/package.json +33 -32
- package/dist/controllers/mcp.js +0 -31
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import "../packages/types/dist/index.js";
|
|
1
2
|
import { getCache } from "../cache.js";
|
|
2
3
|
import { getHelpers } from "../database/helpers/index.js";
|
|
3
4
|
import emitter_default from "../emitter.js";
|
|
@@ -8,10 +9,10 @@ import { splitRecursive } from "../utils/versioning/split-recursive.js";
|
|
|
8
9
|
import { ItemsService } from "./items.js";
|
|
9
10
|
import { ActivityService } from "./activity.js";
|
|
10
11
|
import { RevisionsService } from "./revisions.js";
|
|
11
|
-
import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from "@directus/errors";
|
|
12
|
+
import { ForbiddenError, InvalidPayloadError, UnprocessableContentError, VersionHashMismatchError } from "@directus/errors";
|
|
12
13
|
import { deepMapWithSchema } from "@directus/utils";
|
|
13
|
-
import { assign, get, isEqual, isPlainObject, pick } from "lodash-es";
|
|
14
|
-
import { Action } from "@directus/constants";
|
|
14
|
+
import { assign, get, isEqual, isNil, isPlainObject, pick } from "lodash-es";
|
|
15
|
+
import { Action, VERSION_KEY_DRAFT, isPublishedVersionKey } from "@directus/constants";
|
|
15
16
|
import hash from "object-hash";
|
|
16
17
|
import Joi from "joi";
|
|
17
18
|
|
|
@@ -21,16 +22,23 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
21
22
|
super("directus_versions", options);
|
|
22
23
|
}
|
|
23
24
|
async validateCreateData(data) {
|
|
24
|
-
const
|
|
25
|
+
const versionCreateSchema = Joi.object({
|
|
25
26
|
key: Joi.string().required(),
|
|
26
27
|
name: Joi.string().allow(null),
|
|
27
28
|
collection: Joi.string().required(),
|
|
28
|
-
item: Joi.string().
|
|
29
|
-
})
|
|
29
|
+
item: Joi.string().allow(null)
|
|
30
|
+
});
|
|
31
|
+
const itemLess = isNil(data["item"]);
|
|
32
|
+
const { error } = versionCreateSchema.validate(data);
|
|
30
33
|
if (error) throw new InvalidPayloadError({ reason: error.message });
|
|
31
|
-
if (data["key"]
|
|
34
|
+
if (isPublishedVersionKey(data["key"])) throw new InvalidPayloadError({ reason: `"${data["key"]}" is a reserved version key` });
|
|
35
|
+
if (itemLess && data["key"] !== VERSION_KEY_DRAFT) throw new InvalidPayloadError({ reason: `"key" must be "${VERSION_KEY_DRAFT}" for versions not linked to an item` });
|
|
32
36
|
if (this.accountability) try {
|
|
33
|
-
await validateAccess({
|
|
37
|
+
await validateAccess(itemLess ? {
|
|
38
|
+
accountability: this.accountability,
|
|
39
|
+
action: "read",
|
|
40
|
+
collection: data["collection"]
|
|
41
|
+
} : {
|
|
34
42
|
accountability: this.accountability,
|
|
35
43
|
action: "read",
|
|
36
44
|
collection: data["collection"],
|
|
@@ -47,6 +55,9 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
47
55
|
knex: this.knex,
|
|
48
56
|
schema: this.schema
|
|
49
57
|
}).readOne(data["collection"])).meta?.versioning) throw new UnprocessableContentError({ reason: `Content Versioning is not enabled for collection "${data["collection"]}"` });
|
|
58
|
+
const isSingleton = !!this.schema.collections[data["collection"]]?.singleton;
|
|
59
|
+
if (itemLess) if (isSingleton) await this.assertSingletonEmpty(data["collection"]);
|
|
60
|
+
else return;
|
|
50
61
|
if ((await new VersionsService({
|
|
51
62
|
knex: this.knex,
|
|
52
63
|
schema: this.schema
|
|
@@ -55,9 +66,9 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
55
66
|
filter: {
|
|
56
67
|
key: { _eq: data["key"] },
|
|
57
68
|
collection: { _eq: data["collection"] },
|
|
58
|
-
item: { _eq: data["item"] }
|
|
69
|
+
item: itemLess ? { _null: true } : { _eq: data["item"] }
|
|
59
70
|
}
|
|
60
|
-
}))[0]["count"] > 0) throw new UnprocessableContentError({ reason: `Version "${data["key"]}" already exists for item "${data["item"]}" in collection "${data["collection"]}"` });
|
|
71
|
+
}))[0]["count"] > 0) throw new UnprocessableContentError({ reason: itemLess ? `Singleton collection "${data["collection"]}" already has an item-less version` : `Version "${data["key"]}" already exists for item "${data["item"]}" in collection "${data["collection"]}"` });
|
|
61
72
|
}
|
|
62
73
|
async getMainItem(collection, item, query) {
|
|
63
74
|
return await new ItemsService(collection, {
|
|
@@ -73,18 +84,27 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
73
84
|
mainHash
|
|
74
85
|
};
|
|
75
86
|
}
|
|
76
|
-
async
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
async getVersionSaves(key, collection, item, mapDelta = true) {
|
|
88
|
+
let itemFilter = {};
|
|
89
|
+
if (item) itemFilter = { item: { _eq: item } };
|
|
90
|
+
let versions = await this.readByQuery({
|
|
91
|
+
filter: {
|
|
92
|
+
key: { _eq: key },
|
|
93
|
+
collection: { _eq: collection },
|
|
94
|
+
...itemFilter
|
|
95
|
+
},
|
|
96
|
+
limit: -1
|
|
97
|
+
});
|
|
98
|
+
if (mapDelta) versions = versions.map((version) => {
|
|
99
|
+
if (version.delta) version.delta = this.mapDelta(version);
|
|
100
|
+
return version;
|
|
101
|
+
});
|
|
102
|
+
return versions;
|
|
84
103
|
}
|
|
85
104
|
async createOne(data, opts) {
|
|
86
105
|
await this.validateCreateData(data);
|
|
87
|
-
data["hash"] = hash(await this.getMainItem(data["collection"], data["item"]));
|
|
106
|
+
if (data["item"]) data["hash"] = hash(await this.getMainItem(data["collection"], data["item"]));
|
|
107
|
+
else data["hash"] = null;
|
|
88
108
|
return super.createOne(data, opts);
|
|
89
109
|
}
|
|
90
110
|
async readOne(key, query = {}, opts) {
|
|
@@ -96,6 +116,7 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
96
116
|
if (!Array.isArray(data)) throw new InvalidPayloadError({ reason: "Input should be an array of items" });
|
|
97
117
|
const keyCombos = /* @__PURE__ */ new Set();
|
|
98
118
|
for (const item of data) {
|
|
119
|
+
if (isNil(item["item"])) continue;
|
|
99
120
|
const keyCombo = `${item["key"]}-${item["collection"]}-${item["item"]}`;
|
|
100
121
|
if (keyCombos.has(keyCombo)) throw new UnprocessableContentError({ reason: `Cannot create multiple versions on "${item["item"]}" in collection "${item["collection"]}" with the same key "${item["key"]}"` });
|
|
101
122
|
keyCombos.add(keyCombo);
|
|
@@ -105,31 +126,52 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
105
126
|
async updateMany(keys, data, opts) {
|
|
106
127
|
const { error } = Joi.object({
|
|
107
128
|
key: Joi.string(),
|
|
108
|
-
name: Joi.string().allow(null)
|
|
129
|
+
name: Joi.string().allow(null),
|
|
130
|
+
item: Joi.string().allow(null)
|
|
109
131
|
}).validate(data);
|
|
110
132
|
if (error) throw new InvalidPayloadError({ reason: error.message });
|
|
111
|
-
if ("key"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
if (isPublishedVersionKey(data["key"])) throw new InvalidPayloadError({ reason: `"${data["key"]}" is a reserved version key` });
|
|
134
|
+
const keyCombos = /* @__PURE__ */ new Set();
|
|
135
|
+
for (const pk of keys) {
|
|
136
|
+
const existingVersion = await this.readOne(pk, { fields: [
|
|
137
|
+
"collection",
|
|
138
|
+
"item",
|
|
139
|
+
"key"
|
|
140
|
+
] });
|
|
141
|
+
const collection = existingVersion.collection;
|
|
142
|
+
const item = "item" in data ? data["item"] : existingVersion.item;
|
|
143
|
+
const key = "key" in data ? data["key"] : existingVersion.key;
|
|
144
|
+
if (key !== VERSION_KEY_DRAFT && item === null) throw new InvalidPayloadError({ reason: `"key" must be "${VERSION_KEY_DRAFT}" for versions not linked to an item` });
|
|
145
|
+
if (item === null) {
|
|
146
|
+
if (this.schema.collections[collection]?.singleton) {
|
|
147
|
+
await this.assertSingletonEmpty(collection);
|
|
148
|
+
if ((await super.readByQuery({
|
|
149
|
+
aggregate: { count: ["*"] },
|
|
150
|
+
filter: {
|
|
151
|
+
id: { _neq: pk },
|
|
152
|
+
collection: { _eq: collection },
|
|
153
|
+
item: { _null: true }
|
|
154
|
+
}
|
|
155
|
+
}))[0]["count"] > 0) throw new UnprocessableContentError({ reason: `Singleton collection "${collection}" already has an item-less version` });
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
128
158
|
}
|
|
159
|
+
const keyCombo = `${key}-${collection}-${item}`;
|
|
160
|
+
if (keyCombos.has(keyCombo)) throw new UnprocessableContentError({ reason: `Cannot update multiple versions on "${item}" in collection "${collection}" to the same key "${key}"` });
|
|
161
|
+
keyCombos.add(keyCombo);
|
|
162
|
+
if ((await super.readByQuery({
|
|
163
|
+
aggregate: { count: ["*"] },
|
|
164
|
+
filter: {
|
|
165
|
+
id: { _neq: pk },
|
|
166
|
+
key: { _eq: key },
|
|
167
|
+
collection: { _eq: collection },
|
|
168
|
+
item: { _eq: item }
|
|
169
|
+
}
|
|
170
|
+
}))[0]["count"] > 0) throw new UnprocessableContentError({ reason: `Version "${key}" already exists for item "${item}" in collection "${collection}"` });
|
|
129
171
|
}
|
|
130
172
|
return super.updateMany(keys, data, opts);
|
|
131
173
|
}
|
|
132
|
-
async save(key, delta) {
|
|
174
|
+
async save(key, delta, opts) {
|
|
133
175
|
const version = await super.readOne(key);
|
|
134
176
|
const payloadService = new PayloadService(this.collection, {
|
|
135
177
|
accountability: this.accountability,
|
|
@@ -137,35 +179,65 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
137
179
|
schema: this.schema
|
|
138
180
|
});
|
|
139
181
|
const { item, collection, delta: existingDelta } = version;
|
|
140
|
-
const helpers = getHelpers(this.knex);
|
|
141
182
|
let revisionDelta = await payloadService.prepareDelta(delta);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
183
|
+
if (item) {
|
|
184
|
+
const trackingAccountability = this.schema.collections[collection]?.accountability ?? null;
|
|
185
|
+
if (trackingAccountability !== null) {
|
|
186
|
+
const revisionsService = new RevisionsService({
|
|
187
|
+
knex: this.knex,
|
|
188
|
+
schema: this.schema
|
|
189
|
+
});
|
|
190
|
+
let patchedExistingRevision = false;
|
|
191
|
+
if (opts?.patchRevision && trackingAccountability === "all") {
|
|
192
|
+
const [latestRevision] = await revisionsService.readByQuery({
|
|
193
|
+
filter: { version: { _eq: key } },
|
|
194
|
+
sort: ["-activity.timestamp"],
|
|
195
|
+
limit: 1,
|
|
196
|
+
fields: [
|
|
197
|
+
"id",
|
|
198
|
+
"data",
|
|
199
|
+
"delta",
|
|
200
|
+
"activity.user"
|
|
201
|
+
]
|
|
202
|
+
});
|
|
203
|
+
const currentUser = this.accountability?.user ?? null;
|
|
204
|
+
const latestRevisionUser = (latestRevision?.["activity"])?.user ?? null;
|
|
205
|
+
if (latestRevision && latestRevisionUser === currentUser) {
|
|
206
|
+
const mergedRevisionData = assign({}, latestRevision["data"], revisionDelta);
|
|
207
|
+
const mergedRevisionDelta = assign({}, latestRevision["delta"], revisionDelta);
|
|
208
|
+
await revisionsService.updateOne(latestRevision["id"], {
|
|
209
|
+
data: mergedRevisionData,
|
|
210
|
+
delta: mergedRevisionDelta
|
|
211
|
+
});
|
|
212
|
+
patchedExistingRevision = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!patchedExistingRevision) {
|
|
216
|
+
const activity = await new ActivityService({
|
|
217
|
+
knex: this.knex,
|
|
218
|
+
schema: this.schema
|
|
219
|
+
}).createOne({
|
|
220
|
+
action: Action.VERSION_SAVE,
|
|
221
|
+
user: this.accountability?.user ?? null,
|
|
222
|
+
collection,
|
|
223
|
+
ip: this.accountability?.ip ?? null,
|
|
224
|
+
user_agent: this.accountability?.userAgent ?? null,
|
|
225
|
+
origin: this.accountability?.origin ?? null,
|
|
226
|
+
item
|
|
227
|
+
});
|
|
228
|
+
if (trackingAccountability === "all") await revisionsService.createOne({
|
|
229
|
+
activity,
|
|
230
|
+
version: key,
|
|
231
|
+
collection,
|
|
232
|
+
item,
|
|
233
|
+
data: revisionDelta,
|
|
234
|
+
delta: revisionDelta
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
167
238
|
}
|
|
168
239
|
revisionDelta = revisionDelta ? revisionDelta : null;
|
|
240
|
+
const helpers = getHelpers(this.knex);
|
|
169
241
|
const date = new Date(helpers.date.writeTimestamp((/* @__PURE__ */ new Date()).toISOString()));
|
|
170
242
|
deepMapObjects(revisionDelta, (object, path) => {
|
|
171
243
|
const existing = get(existingDelta, path);
|
|
@@ -186,22 +258,29 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
186
258
|
if (shouldClearCache(cache, void 0, collection)) cache.clear();
|
|
187
259
|
return finalVersionDelta;
|
|
188
260
|
}
|
|
189
|
-
async promote(version,
|
|
261
|
+
async promote(version, opts) {
|
|
190
262
|
const { collection, item, delta } = await super.readOne(version);
|
|
191
|
-
if (
|
|
263
|
+
if (item && typeof opts?.mainHash !== "string") throw new InvalidPayloadError({ reason: `"mainHash" field is required` });
|
|
264
|
+
if (this.accountability) await validateAccess(item ? {
|
|
192
265
|
accountability: this.accountability,
|
|
193
266
|
action: "update",
|
|
194
267
|
collection,
|
|
195
268
|
primaryKeys: [item]
|
|
269
|
+
} : {
|
|
270
|
+
accountability: this.accountability,
|
|
271
|
+
action: "create",
|
|
272
|
+
collection
|
|
196
273
|
}, {
|
|
197
274
|
schema: this.schema,
|
|
198
275
|
knex: this.knex
|
|
199
276
|
});
|
|
200
277
|
if (!delta) throw new UnprocessableContentError({ reason: `No changes to promote` });
|
|
201
|
-
|
|
202
|
-
|
|
278
|
+
if (item) {
|
|
279
|
+
const { outdated, mainHash } = await this.verifyHash(collection, item, opts?.mainHash);
|
|
280
|
+
if (outdated) throw new VersionHashMismatchError({ mainHash });
|
|
281
|
+
}
|
|
203
282
|
const { rawDelta, defaultOverwrites } = splitRecursive(delta);
|
|
204
|
-
const payloadToUpdate = fields ? pick(rawDelta, fields) : rawDelta;
|
|
283
|
+
const payloadToUpdate = opts?.fields ? pick(rawDelta, opts.fields) : rawDelta;
|
|
205
284
|
const itemsService = new ItemsService(collection, {
|
|
206
285
|
accountability: this.accountability,
|
|
207
286
|
knex: this.knex,
|
|
@@ -216,7 +295,13 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
216
295
|
schema: this.schema,
|
|
217
296
|
accountability: this.accountability
|
|
218
297
|
});
|
|
219
|
-
|
|
298
|
+
let updatedItemKey;
|
|
299
|
+
if (item) updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks, { overwriteDefaults: defaultOverwrites });
|
|
300
|
+
else {
|
|
301
|
+
await this.assertSingletonEmpty(collection);
|
|
302
|
+
updatedItemKey = await itemsService.createOne(payloadAfterHooks, { overwriteDefaults: defaultOverwrites });
|
|
303
|
+
await this.updateOne(version, { item: String(updatedItemKey) });
|
|
304
|
+
}
|
|
220
305
|
emitter_default.emitAction(["items.promote", `${collection}.items.promote`], {
|
|
221
306
|
payload: payloadAfterHooks,
|
|
222
307
|
collection,
|
|
@@ -229,6 +314,11 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
229
314
|
});
|
|
230
315
|
return updatedItemKey;
|
|
231
316
|
}
|
|
317
|
+
async assertSingletonEmpty(collection) {
|
|
318
|
+
const collectionMeta = this.schema.collections[collection];
|
|
319
|
+
if (!collectionMeta?.singleton) return;
|
|
320
|
+
if (await this.knex(collection).first(collectionMeta.primary)) throw new UnprocessableContentError({ reason: `Singleton collection "${collection}" already contains an item` });
|
|
321
|
+
}
|
|
232
322
|
mapDelta(version) {
|
|
233
323
|
const delta = version.delta ?? {};
|
|
234
324
|
delta[this.schema.collections[version.collection].primary] = version.item;
|
|
@@ -39,6 +39,7 @@ function calculateFieldDepth(obj, dotNotationKeys = []) {
|
|
|
39
39
|
const keys = Object.keys(obj);
|
|
40
40
|
for (const key of keys) {
|
|
41
41
|
const nestedValue = obj[key];
|
|
42
|
+
if (key === "_json") continue;
|
|
42
43
|
if (dotNotationKeys.includes(key) && nestedValue) {
|
|
43
44
|
let sortDepth = 0;
|
|
44
45
|
for (const sortKey of nestedValue) if (sortKey) sortDepth = Math.max(sortKey.split(".").length, sortDepth);
|
|
@@ -27,6 +27,9 @@ const defaultAdminPolicy = {
|
|
|
27
27
|
async function createAdmin(schema, admin) {
|
|
28
28
|
const logger = useLogger();
|
|
29
29
|
const env = useEnv();
|
|
30
|
+
const adminEmail = admin?.email ?? env["ADMIN_EMAIL"];
|
|
31
|
+
const adminPassword = admin?.password ?? env["ADMIN_PASSWORD"];
|
|
32
|
+
if (!adminEmail || !adminPassword) return;
|
|
30
33
|
logger.info("Setting up first admin role...");
|
|
31
34
|
const accessService = new AccessService({ schema });
|
|
32
35
|
const policiesService = new PoliciesService({ schema });
|
|
@@ -37,9 +40,6 @@ async function createAdmin(schema, admin) {
|
|
|
37
40
|
role
|
|
38
41
|
});
|
|
39
42
|
const usersService = new UsersService({ schema });
|
|
40
|
-
const adminEmail = admin?.email ?? env["ADMIN_EMAIL"];
|
|
41
|
-
const adminPassword = admin?.password ?? env["ADMIN_PASSWORD"];
|
|
42
|
-
if (!adminEmail || !adminPassword) return;
|
|
43
43
|
const token = env["ADMIN_TOKEN"] ?? null;
|
|
44
44
|
logger.info("Adding first admin user...");
|
|
45
45
|
await usersService.createOne({
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isPlainObject } from "lodash-es";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/deep-freeze.ts
|
|
4
|
+
/**
|
|
5
|
+
* Recursively freezes arrays and plain objects so the entire structure is immutable.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const frozen = deepFreeze({ a: { b: 1 } });
|
|
9
|
+
* frozen.a.b = 2; // throws in strict mode
|
|
10
|
+
*/
|
|
11
|
+
function deepFreeze(value) {
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
for (const item of value) deepFreeze(item);
|
|
14
|
+
return Object.freeze(value);
|
|
15
|
+
}
|
|
16
|
+
if (isPlainObject(value)) {
|
|
17
|
+
for (const item of Object.values(value)) deepFreeze(item);
|
|
18
|
+
return Object.freeze(value);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
export { deepFreeze };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/utils/extract-function-name.ts
|
|
2
|
+
/**
|
|
3
|
+
* Extracts the function name from a function call string, e.g. `year(date_created)` → `'year'`.
|
|
4
|
+
* Returns null if the string is not a recognized function call.
|
|
5
|
+
*/
|
|
6
|
+
function extractFunctionName(str) {
|
|
7
|
+
const trimmed = str.trim();
|
|
8
|
+
if (!trimmed.includes("(") || !trimmed.endsWith(")")) return null;
|
|
9
|
+
return trimmed.split("(")[0] ?? null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
export { extractFunctionName };
|
|
@@ -10,7 +10,7 @@ import { cloneFields, validateFieldsEligibility } from "./translations-shared.js
|
|
|
10
10
|
import { dbSafeIdentifierSchema } from "./translations-validation.js";
|
|
11
11
|
import { InvalidPayloadError } from "@directus/errors";
|
|
12
12
|
import { fromZodError } from "zod-validation-error";
|
|
13
|
-
import { z } from "zod";
|
|
13
|
+
import { z as z$1 } from "zod";
|
|
14
14
|
|
|
15
15
|
//#region src/utils/generate-translations.ts
|
|
16
16
|
const logger = useLogger();
|
|
@@ -31,15 +31,15 @@ function buildTranslationsAliasField(languagesFields) {
|
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
const GenerateTranslationsInput = z.object({
|
|
34
|
+
const GenerateTranslationsInput = z$1.object({
|
|
35
35
|
collection: dbSafeIdentifierSchema,
|
|
36
|
-
fields: z.array(dbSafeIdentifierSchema).min(1),
|
|
36
|
+
fields: z$1.array(dbSafeIdentifierSchema).min(1),
|
|
37
37
|
translationsCollection: dbSafeIdentifierSchema.optional(),
|
|
38
38
|
languagesCollection: dbSafeIdentifierSchema.optional(),
|
|
39
39
|
parentFkField: dbSafeIdentifierSchema.optional(),
|
|
40
40
|
languageFkField: dbSafeIdentifierSchema.optional(),
|
|
41
|
-
createLanguagesCollection: z.boolean().optional().default(true),
|
|
42
|
-
seedLanguages: z.boolean().optional().default(true)
|
|
41
|
+
createLanguagesCollection: z$1.boolean().optional().default(true),
|
|
42
|
+
seedLanguages: z$1.boolean().optional().default(true)
|
|
43
43
|
});
|
|
44
44
|
async function generateTranslations(input, options) {
|
|
45
45
|
const parseResult = GenerateTranslationsInput.safeParse(input);
|
|
@@ -5,6 +5,7 @@ import { getSecret } from "./get-secret.js";
|
|
|
5
5
|
import { createDefaultAccountability } from "../permissions/utils/create-default-accountability.js";
|
|
6
6
|
import { verifyAccessJWT } from "./jwt.js";
|
|
7
7
|
import isDirectusJWT from "./is-directus-jwt.js";
|
|
8
|
+
import { parseOAuthScope } from "./parse-oauth-scope.js";
|
|
8
9
|
import { verifySessionJWT } from "./verify-session-jwt.js";
|
|
9
10
|
import { InvalidCredentialsError } from "@directus/errors";
|
|
10
11
|
|
|
@@ -15,8 +16,19 @@ async function getAccountabilityForToken(token, accountability) {
|
|
|
15
16
|
if (token) if (isDirectusJWT(token)) {
|
|
16
17
|
const payload = verifyAccessJWT(token, getSecret());
|
|
17
18
|
if ("session" in payload) {
|
|
18
|
-
await verifySessionJWT(payload);
|
|
19
|
+
const { oauth_client } = await verifySessionJWT(payload);
|
|
19
20
|
accountability.session = payload.session;
|
|
21
|
+
if (oauth_client !== null) {
|
|
22
|
+
let aud;
|
|
23
|
+
if (Array.isArray(payload.aud)) aud = payload.aud;
|
|
24
|
+
else if (payload.aud) aud = [String(payload.aud)];
|
|
25
|
+
else aud = [];
|
|
26
|
+
accountability.oauth = {
|
|
27
|
+
client: oauth_client,
|
|
28
|
+
scopes: parseOAuthScope(payload.scope),
|
|
29
|
+
aud
|
|
30
|
+
};
|
|
31
|
+
}
|
|
20
32
|
}
|
|
21
33
|
if (payload.share) accountability.share = payload.share;
|
|
22
34
|
if (payload.id) accountability.user = payload.id;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import database_default from "../database/index.js";
|
|
2
|
+
import { getFlowManager } from "../flows.js";
|
|
2
3
|
import { fetchPoliciesIpAccess } from "../permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.js";
|
|
3
4
|
import { getGraphqlQueryAndVariables } from "./get-graphql-query-and-variables.js";
|
|
4
|
-
import { getFlowManager } from "../flows.js";
|
|
5
5
|
import { toArray } from "@directus/utils";
|
|
6
6
|
import { isEmpty, pick } from "lodash-es";
|
|
7
7
|
import { ipInNetworks } from "@directus/utils/node";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getEntitlementManager } from "../license/entitlements/manager.js";
|
|
2
|
+
import "../license/index.js";
|
|
3
|
+
import { mergeFilters } from "@directus/utils";
|
|
4
|
+
|
|
5
|
+
//#region src/utils/get-history-filter-query.ts
|
|
6
|
+
function getHistoryFilterQuery(query, entitlement, buildFilter) {
|
|
7
|
+
const limit = getEntitlementManager().getEntitlementLimit(entitlement);
|
|
8
|
+
if (limit === null || !Number.isFinite(limit) || limit < 0) return query;
|
|
9
|
+
if (limit === 0) return {
|
|
10
|
+
...query,
|
|
11
|
+
limit: 0
|
|
12
|
+
};
|
|
13
|
+
const filter = mergeFilters(buildFilter(/* @__PURE__ */ new Date(Date.now() - limit * 1e3)), query.filter ?? null, "and");
|
|
14
|
+
if (!filter) return query;
|
|
15
|
+
return {
|
|
16
|
+
...query,
|
|
17
|
+
filter
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { getHistoryFilterQuery };
|
package/dist/utils/get-schema.js
CHANGED
|
@@ -12,7 +12,7 @@ import getDefaultValue from "./get-default-value.js";
|
|
|
12
12
|
import { getSystemFieldRowsWithAuthProviders } from "./get-field-system-rows.js";
|
|
13
13
|
import { useEnv } from "@directus/env";
|
|
14
14
|
import { parseJSON, toArray, toBoolean } from "@directus/utils";
|
|
15
|
-
import { mapValues } from "lodash-es";
|
|
15
|
+
import { mapValues, pick } from "lodash-es";
|
|
16
16
|
import { createInspector } from "@directus/schema";
|
|
17
17
|
import { systemCollectionRows } from "@directus/system-data";
|
|
18
18
|
|
|
@@ -74,7 +74,7 @@ async function getDatabaseSchema(database, schemaInspector) {
|
|
|
74
74
|
};
|
|
75
75
|
const systemFieldRows$1 = getSystemFieldRowsWithAuthProviders();
|
|
76
76
|
const schemaOverview = await schemaInspector.overview();
|
|
77
|
-
const collections = [...await database.select("collection", "singleton", "note", "sort_field", "accountability")
|
|
77
|
+
const collections = [...(await database.select("*").from("directus_collections")).filter((c) => !("status" in c) || c["status"] === "active").map((c) => pick(c, "collection", "singleton", "note", "sort_field", "accountability")), ...systemCollectionRows];
|
|
78
78
|
for (const [collection, info] of Object.entries(schemaOverview)) {
|
|
79
79
|
if (toArray(env["DB_EXCLUDE_TABLES"]).includes(collection)) {
|
|
80
80
|
logger.trace(`Collection "${collection}" is configured to be excluded and will be ignored`);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { ItemsService } from "../services/items.js";
|
|
2
2
|
import { FilesService } from "../services/files.js";
|
|
3
3
|
import { FoldersService } from "../services/folders.js";
|
|
4
|
-
import { ActivityService } from "../services/activity.js";
|
|
5
4
|
import { AccessService } from "../services/access.js";
|
|
5
|
+
import { ActivityService } from "../services/activity.js";
|
|
6
|
+
import { FlowsService } from "../services/flows.js";
|
|
7
|
+
import { RevisionsService } from "../services/revisions.js";
|
|
6
8
|
import { SettingsService } from "../services/settings.js";
|
|
7
9
|
import { UsersService } from "../services/users.js";
|
|
8
10
|
import { NotificationsService } from "../services/notifications.js";
|
|
9
|
-
import { RevisionsService } from "../services/revisions.js";
|
|
10
11
|
import { CommentsService } from "../services/comments.js";
|
|
11
12
|
import { DashboardsService } from "../services/dashboards.js";
|
|
12
13
|
import { DeploymentProjectsService } from "../services/deployment-projects.js";
|
|
@@ -22,7 +23,6 @@ import { SharesService } from "../services/shares.js";
|
|
|
22
23
|
import { TranslationsService } from "../services/translations.js";
|
|
23
24
|
import { VersionsService } from "../services/versions.js";
|
|
24
25
|
import "../services/index.js";
|
|
25
|
-
import { FlowsService } from "../services/flows.js";
|
|
26
26
|
import { ForbiddenError } from "@directus/errors";
|
|
27
27
|
|
|
28
28
|
//#region src/utils/get-service.ts
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/utils/is-unauthenticated.ts
|
|
2
|
+
/**
|
|
3
|
+
* Checks if the given accountability is unauthenticated
|
|
4
|
+
*
|
|
5
|
+
* @param accountability
|
|
6
|
+
* @returns True if the user is unauthenticated, false otherwise.
|
|
7
|
+
*/
|
|
8
|
+
function isUnauthenticated(accountability) {
|
|
9
|
+
if (accountability === null) return false;
|
|
10
|
+
if (accountability === void 0) return true;
|
|
11
|
+
return accountability?.role === null && accountability?.user === null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
export { isUnauthenticated };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/utils/parse-oauth-scope.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parse an OAuth scope string into a deduplicated array of scope tokens.
|
|
4
|
+
* Per RFC 6749 Section 3.3, scope is a space-separated list of case-sensitive strings.
|
|
5
|
+
*/
|
|
6
|
+
function parseOAuthScope(scope) {
|
|
7
|
+
if (typeof scope !== "string" || scope.trim() === "") return [];
|
|
8
|
+
return [...new Set(scope.trim().split(/\s+/))];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { parseOAuthScope };
|
|
@@ -61,7 +61,7 @@ function sanitizeFields(rawFields) {
|
|
|
61
61
|
}
|
|
62
62
|
function sanitizeSort(rawSort) {
|
|
63
63
|
let fields = [];
|
|
64
|
-
if (typeof rawSort === "string") fields = rawSort
|
|
64
|
+
if (typeof rawSort === "string") fields = splitFields(rawSort);
|
|
65
65
|
else if (Array.isArray(rawSort)) fields = rawSort;
|
|
66
66
|
fields = fields.map((field) => field.trim());
|
|
67
67
|
return fields;
|
|
@@ -143,7 +143,7 @@ async function sanitizeDeep(deep, schema, accountability) {
|
|
|
143
143
|
for (const [key, value] of Object.entries(level)) {
|
|
144
144
|
if (!key) break;
|
|
145
145
|
if (key.startsWith("_")) subQuery[key.substring(1)] = value;
|
|
146
|
-
else if (isPlainObject(value)) parse(value, [...path, key]);
|
|
146
|
+
else if (isPlainObject(value)) await parse(value, [...path, key]);
|
|
147
147
|
}
|
|
148
148
|
if (Object.keys(subQuery).length > 0) {
|
|
149
149
|
const parsedSubQuery = await sanitizeQuery(subQuery, schema, accountability);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/utils/split-field-path.ts
|
|
2
|
+
/**
|
|
3
|
+
* Splits a field path on dots that are outside parentheses.
|
|
4
|
+
*
|
|
5
|
+
* A plain `.split('.')` breaks when a path segment contains a function call
|
|
6
|
+
* whose arguments include dots (e.g. `category_id.json(metadata, settings.theme)`).
|
|
7
|
+
* This helper only splits on dots at paren-depth 0.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* splitFieldPath('a.b.c') // ['a', 'b', 'c']
|
|
11
|
+
* splitFieldPath('category_id.json(metadata, color)') // ['category_id', 'json(metadata, color)']
|
|
12
|
+
* splitFieldPath('a.json(meta, x.y.z)') // ['a', 'json(meta, x.y.z)']
|
|
13
|
+
*/
|
|
14
|
+
function splitFieldPath(path) {
|
|
15
|
+
const parts = [];
|
|
16
|
+
let depth = 0;
|
|
17
|
+
let start = 0;
|
|
18
|
+
for (let i = 0; i < path.length; i++) if (path[i] === "(") depth++;
|
|
19
|
+
else if (path[i] === ")") depth--;
|
|
20
|
+
else if (path[i] === "." && depth === 0) {
|
|
21
|
+
parts.push(path.slice(start, i));
|
|
22
|
+
start = i + 1;
|
|
23
|
+
}
|
|
24
|
+
parts.push(path.slice(start));
|
|
25
|
+
return parts;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { splitFieldPath };
|
package/dist/utils/store.js
CHANGED
|
@@ -13,7 +13,7 @@ function useStore(namespace, options) {
|
|
|
13
13
|
namespace,
|
|
14
14
|
redis: useRedis()
|
|
15
15
|
};
|
|
16
|
-
if (
|
|
16
|
+
if (options?.ttl) config.ttl = options?.ttl;
|
|
17
17
|
const store = createCache(config);
|
|
18
18
|
return (callback) => store.usingLock(`lock`, async () => {
|
|
19
19
|
return await callback({
|