@directus/api 36.0.0-rc.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/app.js +2 -2
- package/dist/controllers/server.js +3 -4
- package/dist/database/run-ast/utils/merge-with-parent-items.js +5 -3
- package/dist/services/graphql/resolvers/system.js +9 -9
- package/dist/services/graphql/schema/get-types.js +5 -5
- package/dist/services/server.js +45 -64
- package/dist/services/users.js +10 -0
- package/dist/services/versions.js +26 -4
- package/dist/utils/create-admin.js +3 -3
- package/dist/utils/is-unauthenticated.js +15 -0
- package/dist/utils/sanitize-query.js +1 -1
- package/dist/utils/store.js +1 -1
- package/dist/utils/versioning/handle-version.js +2 -1
- package/package.json +21 -21
package/dist/app.js
CHANGED
|
@@ -11,8 +11,6 @@ import { getFlowManager } from "./flows.js";
|
|
|
11
11
|
import { getExtensionManager } from "./extensions/index.js";
|
|
12
12
|
import { registerAuthProviders } from "./auth.js";
|
|
13
13
|
import { ensureDeploymentWebhooks, registerDeploymentDrivers } from "./deployment.js";
|
|
14
|
-
import rate_limiter_global_default from "./middleware/rate-limiter-global.js";
|
|
15
|
-
import rate_limiter_ip_default from "./middleware/rate-limiter-ip.js";
|
|
16
14
|
import { getLicenseManager } from "./license/manager.js";
|
|
17
15
|
import "./license/index.js";
|
|
18
16
|
import { aiRouter } from "./ai/chat/router.js";
|
|
@@ -63,6 +61,8 @@ import cors_default from "./middleware/cors.js";
|
|
|
63
61
|
import { errorHandler } from "./middleware/error-handler.js";
|
|
64
62
|
import extract_token_default from "./middleware/extract-token.js";
|
|
65
63
|
import mcp_oauth_guard_default from "./middleware/mcp-oauth-guard.js";
|
|
64
|
+
import rate_limiter_global_default from "./middleware/rate-limiter-global.js";
|
|
65
|
+
import rate_limiter_ip_default from "./middleware/rate-limiter-ip.js";
|
|
66
66
|
import request_counter_default from "./middleware/request-counter.js";
|
|
67
67
|
import sanitize_query_default from "./middleware/sanitize-query.js";
|
|
68
68
|
import schema_default$1 from "./middleware/schema.js";
|
|
@@ -8,6 +8,7 @@ import { getLicenseManager } from "../license/manager.js";
|
|
|
8
8
|
import { createAdmin } from "../utils/create-admin.js";
|
|
9
9
|
import { useEnv } from "@directus/env";
|
|
10
10
|
import { ErrorCode, ForbiddenError, InvalidPayloadError, RouteNotFoundError, isDirectusError } from "@directus/errors";
|
|
11
|
+
import { toBoolean } from "@directus/utils";
|
|
11
12
|
import { Router } from "express";
|
|
12
13
|
import { fromZodError } from "zod-validation-error";
|
|
13
14
|
import z from "zod";
|
|
@@ -49,7 +50,7 @@ router.get("/info", async_handler_default(async (req, res, next) => {
|
|
|
49
50
|
res.locals["payload"] = { data };
|
|
50
51
|
return next();
|
|
51
52
|
}), respond);
|
|
52
|
-
router.get("/health", async_handler_default(async (req, res, next) => {
|
|
53
|
+
if (toBoolean(env["HEALTHCHECK_ENABLED"]) !== false) router.get("/health", async_handler_default(async (req, res, next) => {
|
|
53
54
|
const data = await new ServerService({
|
|
54
55
|
accountability: req.accountability,
|
|
55
56
|
schema: req.schema
|
|
@@ -85,6 +86,7 @@ router.post("/setup", async_handler_default(async (req, _res, next) => {
|
|
|
85
86
|
if (error) throw new InvalidPayloadError({ reason: fromZodError(error).message });
|
|
86
87
|
const licenseManager = getLicenseManager();
|
|
87
88
|
try {
|
|
89
|
+
if (data.license_key) await licenseManager.activate(data.license_key);
|
|
88
90
|
await createAdmin(req.schema, {
|
|
89
91
|
email: data.admin.email,
|
|
90
92
|
password: data.admin.password,
|
|
@@ -92,9 +94,6 @@ router.post("/setup", async_handler_default(async (req, _res, next) => {
|
|
|
92
94
|
last_name: data.admin.last_name
|
|
93
95
|
});
|
|
94
96
|
const settingsService = new SettingsService({ schema: req.schema });
|
|
95
|
-
if (data.license_key) try {
|
|
96
|
-
await licenseManager.activate(data.license_key);
|
|
97
|
-
} catch {}
|
|
98
97
|
if (data.owner) settingsService.setOwner(data.owner);
|
|
99
98
|
} catch (error$1) {
|
|
100
99
|
if (isDirectusError(error$1, ErrorCode.Forbidden)) return next();
|
|
@@ -9,13 +9,13 @@ function mergeWithParentItems(schema, nestedItem, parentItem, nestedNode, fieldA
|
|
|
9
9
|
const parentItems = clone(toArray(parentItem));
|
|
10
10
|
if (nestedNode.type === "m2o") {
|
|
11
11
|
const parentsByForeignKey = /* @__PURE__ */ new Map();
|
|
12
|
+
const nestedPrimaryKeyField = schema.collections[nestedNode.relation.related_collection].primary;
|
|
12
13
|
for (const parentItem$1 of parentItems) {
|
|
13
|
-
const relationKey = parentItem$1[nestedNode.relation.field];
|
|
14
|
+
const relationKey = parentItem$1[nestedNode.relation.field]?.[nestedPrimaryKeyField] ?? parentItem$1[nestedNode.relation.field];
|
|
14
15
|
if (!parentsByForeignKey.has(relationKey)) parentsByForeignKey.set(relationKey, []);
|
|
15
16
|
parentItem$1[nestedNode.fieldKey] = null;
|
|
16
17
|
parentsByForeignKey.get(relationKey).push(parentItem$1);
|
|
17
18
|
}
|
|
18
|
-
const nestedPrimaryKeyField = schema.collections[nestedNode.relation.related_collection].primary;
|
|
19
19
|
for (const nestedItem$1 of nestedItems) {
|
|
20
20
|
const nestedPK = nestedItem$1[nestedPrimaryKeyField];
|
|
21
21
|
if (nestedPK === null) continue;
|
|
@@ -84,8 +84,10 @@ function mergeWithParentItems(schema, nestedItem, parentItem, nestedNode, fieldA
|
|
|
84
84
|
parentItem$1[nestedNode.fieldKey] = null;
|
|
85
85
|
continue;
|
|
86
86
|
}
|
|
87
|
+
const relatedPrimaryKeyField = schema.collections[relatedCollection].primary;
|
|
88
|
+
const foreignKey = parentItem$1[nestedNode.relation.field]?.[relatedPrimaryKeyField] ?? parentItem$1[nestedNode.relation.field];
|
|
87
89
|
const itemChild = nestedItem[relatedCollection].find((nestedItem$1) => {
|
|
88
|
-
return nestedItem$1[nestedNode.relatedKey[relatedCollection]] ==
|
|
90
|
+
return nestedItem$1[nestedNode.relatedKey[relatedCollection]] == foreignKey;
|
|
89
91
|
});
|
|
90
92
|
parentItem$1[nestedNode.fieldKey] = itemChild || null;
|
|
91
93
|
}
|
|
@@ -160,17 +160,17 @@ function injectSystemResolvers(gql, schemaComposer, { CreateCollectionTypes, Rea
|
|
|
160
160
|
schema: gql.schema
|
|
161
161
|
}).serverInfo();
|
|
162
162
|
}, "server_info")
|
|
163
|
-
},
|
|
164
|
-
server_health: {
|
|
165
|
-
type: GraphQLJSON,
|
|
166
|
-
resolve: dedupeResolver(async () => {
|
|
167
|
-
return await new ServerService({
|
|
168
|
-
accountability: gql.accountability,
|
|
169
|
-
schema: gql.schema
|
|
170
|
-
}).health();
|
|
171
|
-
}, "server_health")
|
|
172
163
|
}
|
|
173
164
|
});
|
|
165
|
+
if (toBoolean(env["HEALTHCHECK_ENABLED"]) !== false) schemaComposer.Query.addFields({ server_health: {
|
|
166
|
+
type: GraphQLJSON,
|
|
167
|
+
resolve: dedupeResolver(async () => {
|
|
168
|
+
return await new ServerService({
|
|
169
|
+
accountability: gql.accountability,
|
|
170
|
+
schema: gql.schema
|
|
171
|
+
}).health();
|
|
172
|
+
}, "server_health")
|
|
173
|
+
} });
|
|
174
174
|
if ("directus_collections" in schema.read.collections) {
|
|
175
175
|
const Collection = getCollectionType(schemaComposer, schema, "read");
|
|
176
176
|
schemaComposer.Query.addFields({
|
|
@@ -119,26 +119,26 @@ function getTypes(schemaComposer, scope, schema, inconsistentFields, action) {
|
|
|
119
119
|
CollectionTypes[relation.collection]?.addFields({ [relation.field]: {
|
|
120
120
|
type: CollectionTypes[relation.related_collection],
|
|
121
121
|
resolve: (obj, _, __, info) => {
|
|
122
|
-
return obj[info?.path?.key ?? relation.field];
|
|
122
|
+
return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.field];
|
|
123
123
|
}
|
|
124
124
|
} });
|
|
125
125
|
VersionTypes[relation.collection]?.addFields({ [relation.field]: {
|
|
126
126
|
type: GraphQLJSON,
|
|
127
127
|
resolve: (obj, _, __, info) => {
|
|
128
|
-
return obj[info?.path?.key ?? relation.field];
|
|
128
|
+
return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.field];
|
|
129
129
|
}
|
|
130
130
|
} });
|
|
131
131
|
if (relation.meta?.one_field) {
|
|
132
132
|
CollectionTypes[relation.related_collection]?.addFields({ [relation.meta.one_field]: {
|
|
133
133
|
type: [CollectionTypes[relation.collection]],
|
|
134
134
|
resolve: (obj, _, __, info) => {
|
|
135
|
-
return obj[info?.path?.key ?? relation.meta.one_field];
|
|
135
|
+
return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.meta.one_field];
|
|
136
136
|
}
|
|
137
137
|
} });
|
|
138
138
|
if (scope === "items") VersionTypes[relation.related_collection]?.addFields({ [relation.meta.one_field]: {
|
|
139
139
|
type: GraphQLJSON,
|
|
140
140
|
resolve: (obj, _, __, info) => {
|
|
141
|
-
return obj[info?.path?.key ?? relation.meta.one_field];
|
|
141
|
+
return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.meta.one_field];
|
|
142
142
|
}
|
|
143
143
|
} });
|
|
144
144
|
}
|
|
@@ -160,7 +160,7 @@ function getTypes(schemaComposer, scope, schema, inconsistentFields, action) {
|
|
|
160
160
|
}
|
|
161
161
|
}),
|
|
162
162
|
resolve: (obj, _, __, info) => {
|
|
163
|
-
return obj[info?.path?.key ?? relation.field];
|
|
163
|
+
return obj[info?.path?.key] ?? obj[info?.fieldName ?? relation.field];
|
|
164
164
|
}
|
|
165
165
|
} });
|
|
166
166
|
return {
|
package/dist/services/server.js
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
|
+
import { useRedis } from "../redis/lib/use-redis.js";
|
|
2
|
+
import { redisConfigAvailable } from "../redis/utils/redis-config-available.js";
|
|
3
|
+
import "../redis/index.js";
|
|
1
4
|
import { useLogger } from "../logger/index.js";
|
|
5
|
+
import { getMilliseconds } from "../utils/get-milliseconds.js";
|
|
2
6
|
import { FILE_UPLOADS, RESUMABLE_UPLOADS } from "../constants.js";
|
|
3
|
-
import { getCache } from "../cache.js";
|
|
4
7
|
import { getStorage } from "../storage/index.js";
|
|
5
8
|
import database_default, { hasDatabaseConnection } from "../database/index.js";
|
|
9
|
+
import { useStore } from "../utils/store.js";
|
|
6
10
|
import getMailer from "../mailer.js";
|
|
7
11
|
import { getEntitlementManager } from "../license/entitlements/manager.js";
|
|
8
12
|
import { SettingsService } from "./settings.js";
|
|
9
|
-
import { rateLimiterGlobal } from "../middleware/rate-limiter-global.js";
|
|
10
|
-
import { rateLimiter } from "../middleware/rate-limiter-ip.js";
|
|
11
13
|
import { getAllowedLogLevels } from "../utils/get-allowed-log-levels.js";
|
|
12
14
|
import { SERVER_ONLINE } from "../server.js";
|
|
15
|
+
import { isUnauthenticated } from "../utils/is-unauthenticated.js";
|
|
13
16
|
import { getLicenseManager } from "../license/manager.js";
|
|
14
17
|
import "../license/index.js";
|
|
15
18
|
import { useEnv } from "@directus/env";
|
|
19
|
+
import { ForbiddenError } from "@directus/errors";
|
|
16
20
|
import { toArray, toBoolean } from "@directus/utils";
|
|
17
21
|
import { merge } from "lodash-es";
|
|
22
|
+
import { createKv } from "@directus/memory";
|
|
18
23
|
import { performance } from "perf_hooks";
|
|
19
24
|
import { Readable } from "node:stream";
|
|
20
25
|
import { version } from "directus/version";
|
|
@@ -22,6 +27,8 @@ import { version } from "directus/version";
|
|
|
22
27
|
//#region src/services/server.ts
|
|
23
28
|
const env = useEnv();
|
|
24
29
|
const logger = useLogger();
|
|
30
|
+
const HEALTHCHECK_CACHE_TTL = getMilliseconds(env["HEALTHCHECK_CACHE_TTL"], 3e5);
|
|
31
|
+
const store = useStore(env["HEALTHCHECK_NAMESPACE"] ?? "directus:healthcheck", { ttl: HEALTHCHECK_CACHE_TTL });
|
|
25
32
|
var ServerService = class {
|
|
26
33
|
knex;
|
|
27
34
|
accountability;
|
|
@@ -121,17 +128,25 @@ var ServerService = class {
|
|
|
121
128
|
return info;
|
|
122
129
|
}
|
|
123
130
|
async health() {
|
|
131
|
+
if (isUnauthenticated(this.accountability)) throw new ForbiddenError();
|
|
132
|
+
const healthResult = await store(async (store$1) => {
|
|
133
|
+
try {
|
|
134
|
+
return await store$1.get("health");
|
|
135
|
+
} catch (err) {
|
|
136
|
+
logger.warn(err, "Failed to read health check cache");
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
if (healthResult) return this.accountability?.admin === true ? healthResult : { status: healthResult["status"] };
|
|
124
140
|
const { nanoid } = await import("nanoid");
|
|
125
141
|
const checkID = nanoid(5);
|
|
142
|
+
const enabledServices = toArray(env["HEALTHCHECK_SERVICES"]);
|
|
126
143
|
const data = {
|
|
127
144
|
status: "ok",
|
|
128
145
|
releaseId: version,
|
|
129
146
|
serviceId: env["PUBLIC_URL"],
|
|
130
147
|
checks: merge(...await Promise.all([
|
|
131
148
|
testDatabase(),
|
|
132
|
-
|
|
133
|
-
testRateLimiter(),
|
|
134
|
-
testRateLimiterGlobal(),
|
|
149
|
+
testRedis(),
|
|
135
150
|
testStorage(),
|
|
136
151
|
testEmail()
|
|
137
152
|
]))
|
|
@@ -152,9 +167,14 @@ var ServerService = class {
|
|
|
152
167
|
}
|
|
153
168
|
if (data.status === "error") break;
|
|
154
169
|
}
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
await store(async (store$1) => {
|
|
171
|
+
await store$1.set("health", data).catch((err) => {
|
|
172
|
+
logger.warn(err, "Failed to write health check cache");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
return this.accountability?.admin === true ? data : { status: data.status };
|
|
157
176
|
async function testDatabase() {
|
|
177
|
+
if (enabledServices.includes("database") === false) return {};
|
|
158
178
|
const database = database_default();
|
|
159
179
|
const client = env["DB_CLIENT"];
|
|
160
180
|
const checks = {};
|
|
@@ -186,10 +206,15 @@ var ServerService = class {
|
|
|
186
206
|
}];
|
|
187
207
|
return checks;
|
|
188
208
|
}
|
|
189
|
-
async function
|
|
190
|
-
if (
|
|
191
|
-
const
|
|
192
|
-
|
|
209
|
+
async function testRedis() {
|
|
210
|
+
if (enabledServices.includes("redis") === false || redisConfigAvailable() !== true) return {};
|
|
211
|
+
const redis = createKv({
|
|
212
|
+
type: "redis",
|
|
213
|
+
redis: useRedis(),
|
|
214
|
+
namespace: env["HEALTHCHECK_NAMESPACE"] ?? "directus:healthcheck",
|
|
215
|
+
ttl: HEALTHCHECK_CACHE_TTL
|
|
216
|
+
});
|
|
217
|
+
const checks = { "redis:responseTime": [{
|
|
193
218
|
status: "ok",
|
|
194
219
|
componentType: "cache",
|
|
195
220
|
observedValue: 0,
|
|
@@ -198,65 +223,20 @@ var ServerService = class {
|
|
|
198
223
|
}] };
|
|
199
224
|
const startTime = performance.now();
|
|
200
225
|
try {
|
|
201
|
-
await
|
|
202
|
-
await
|
|
203
|
-
} catch (err) {
|
|
204
|
-
checks["cache:responseTime"][0].status = "error";
|
|
205
|
-
checks["cache:responseTime"][0].output = err;
|
|
206
|
-
} finally {
|
|
207
|
-
const endTime = performance.now();
|
|
208
|
-
checks["cache:responseTime"][0].observedValue = +(endTime - startTime).toFixed(3);
|
|
209
|
-
if (checks["cache:responseTime"][0].observedValue > checks["cache:responseTime"][0].threshold && checks["cache:responseTime"][0].status !== "error") checks["cache:responseTime"][0].status = "warn";
|
|
210
|
-
}
|
|
211
|
-
return checks;
|
|
212
|
-
}
|
|
213
|
-
async function testRateLimiter() {
|
|
214
|
-
if (env["RATE_LIMITER_ENABLED"] !== true) return {};
|
|
215
|
-
const checks = { "rateLimiter:responseTime": [{
|
|
216
|
-
status: "ok",
|
|
217
|
-
componentType: "ratelimiter",
|
|
218
|
-
observedValue: 0,
|
|
219
|
-
observedUnit: "ms",
|
|
220
|
-
threshold: env["RATE_LIMITER_HEALTHCHECK_THRESHOLD"] ? +env["RATE_LIMITER_HEALTHCHECK_THRESHOLD"] : 150
|
|
221
|
-
}] };
|
|
222
|
-
const startTime = performance.now();
|
|
223
|
-
try {
|
|
224
|
-
await rateLimiter.consume(`directus-health-${checkID}`, 1);
|
|
225
|
-
await rateLimiter.delete(`directus-health-${checkID}`);
|
|
226
|
-
} catch (err) {
|
|
227
|
-
checks["rateLimiter:responseTime"][0].status = "error";
|
|
228
|
-
checks["rateLimiter:responseTime"][0].output = err;
|
|
229
|
-
} finally {
|
|
230
|
-
const endTime = performance.now();
|
|
231
|
-
checks["rateLimiter:responseTime"][0].observedValue = +(endTime - startTime).toFixed(3);
|
|
232
|
-
if (checks["rateLimiter:responseTime"][0].observedValue > checks["rateLimiter:responseTime"][0].threshold && checks["rateLimiter:responseTime"][0].status !== "error") checks["rateLimiter:responseTime"][0].status = "warn";
|
|
233
|
-
}
|
|
234
|
-
return checks;
|
|
235
|
-
}
|
|
236
|
-
async function testRateLimiterGlobal() {
|
|
237
|
-
if (env["RATE_LIMITER_GLOBAL_ENABLED"] !== true) return {};
|
|
238
|
-
const checks = { "rateLimiterGlobal:responseTime": [{
|
|
239
|
-
status: "ok",
|
|
240
|
-
componentType: "ratelimiter",
|
|
241
|
-
observedValue: 0,
|
|
242
|
-
observedUnit: "ms",
|
|
243
|
-
threshold: env["RATE_LIMITER_GLOBAL_HEALTHCHECK_THRESHOLD"] ? +env["RATE_LIMITER_GLOBAL_HEALTHCHECK_THRESHOLD"] : 150
|
|
244
|
-
}] };
|
|
245
|
-
const startTime = performance.now();
|
|
246
|
-
try {
|
|
247
|
-
await rateLimiterGlobal.consume(`directus-health-${checkID}`, 1);
|
|
248
|
-
await rateLimiterGlobal.delete(`directus-health-${checkID}`);
|
|
226
|
+
await redis.set(`directus-health-${checkID}`, 1);
|
|
227
|
+
await redis.delete(`directus-health-${checkID}`);
|
|
249
228
|
} catch (err) {
|
|
250
|
-
checks["
|
|
251
|
-
checks["
|
|
229
|
+
checks["redis:responseTime"][0].status = "error";
|
|
230
|
+
checks["redis:responseTime"][0].output = err;
|
|
252
231
|
} finally {
|
|
253
232
|
const endTime = performance.now();
|
|
254
|
-
checks["
|
|
255
|
-
if (checks["
|
|
233
|
+
checks["redis:responseTime"][0].observedValue = +(endTime - startTime).toFixed(3);
|
|
234
|
+
if (checks["redis:responseTime"][0].observedValue > checks["redis:responseTime"][0].threshold && checks["redis:responseTime"][0].status !== "error") checks["redis:responseTime"][0].status = "warn";
|
|
256
235
|
}
|
|
257
236
|
return checks;
|
|
258
237
|
}
|
|
259
238
|
async function testStorage() {
|
|
239
|
+
if (enabledServices.includes("storage") === false) return {};
|
|
260
240
|
const storage = await getStorage();
|
|
261
241
|
const checks = {};
|
|
262
242
|
for (const location of toArray(env["STORAGE_LOCATIONS"])) {
|
|
@@ -284,6 +264,7 @@ var ServerService = class {
|
|
|
284
264
|
return checks;
|
|
285
265
|
}
|
|
286
266
|
async function testEmail() {
|
|
267
|
+
if (enabledServices.includes("email") === false || toBoolean(env["EMAIL_VERIFY_SETUP"]) === false) return {};
|
|
287
268
|
const checks = { "email:connection": [{
|
|
288
269
|
status: "ok",
|
|
289
270
|
componentType: "email"
|
package/dist/services/users.js
CHANGED
|
@@ -117,6 +117,12 @@ var UsersService = class UsersService extends ItemsService {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
/**
|
|
120
|
+
* Block setting a non-default auth provider when the current license isn't entitled to SSO
|
|
121
|
+
*/
|
|
122
|
+
checkProviderEntitlement(input) {
|
|
123
|
+
if ((Array.isArray(input) ? input : [input]).some((provider) => provider && provider !== DEFAULT_AUTH_PROVIDER) && !getEntitlementManager().isEntitled("sso_enabled")) throw new InvalidPayloadError({ reason: `Setting a custom "provider" isn't included in the current license` });
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
120
126
|
* Create a new user
|
|
121
127
|
*/
|
|
122
128
|
async createOne(data, opts = {}) {
|
|
@@ -126,6 +132,7 @@ var UsersService = class UsersService extends ItemsService {
|
|
|
126
132
|
await this.checkUniqueEmails([data["email"]]);
|
|
127
133
|
}
|
|
128
134
|
if ("password" in data) await this.checkPasswordPolicy([data["password"]]);
|
|
135
|
+
if ("provider" in data) this.checkProviderEntitlement(data["provider"]);
|
|
129
136
|
} catch (err) {
|
|
130
137
|
opts.preMutationError = err;
|
|
131
138
|
}
|
|
@@ -141,6 +148,7 @@ var UsersService = class UsersService extends ItemsService {
|
|
|
141
148
|
async createMany(data, opts = {}) {
|
|
142
149
|
const emails = data.map((payload) => payload["email"]).filter((email) => email);
|
|
143
150
|
const passwords = data.map((payload) => payload["password"]).filter((password) => password);
|
|
151
|
+
const providers = data.map((payload) => payload["provider"]).filter((provider) => provider);
|
|
144
152
|
const someActive = data.some((payload) => !("status" in payload) || payload["status"] === "active");
|
|
145
153
|
try {
|
|
146
154
|
if (emails.length) {
|
|
@@ -148,6 +156,7 @@ var UsersService = class UsersService extends ItemsService {
|
|
|
148
156
|
await this.checkUniqueEmails(emails);
|
|
149
157
|
}
|
|
150
158
|
if (passwords.length) await this.checkPasswordPolicy(passwords);
|
|
159
|
+
if (providers.length) this.checkProviderEntitlement(providers);
|
|
151
160
|
} catch (err) {
|
|
152
161
|
opts.preMutationError = err;
|
|
153
162
|
}
|
|
@@ -179,6 +188,7 @@ var UsersService = class UsersService extends ItemsService {
|
|
|
179
188
|
if (data["tfa_secret"] !== void 0) throw new InvalidPayloadError({ reason: `You can't change the "tfa_secret" value manually` });
|
|
180
189
|
if (data["provider"] !== void 0) {
|
|
181
190
|
if (this.accountability && this.accountability.admin !== true) throw new InvalidPayloadError({ reason: `You can't change the "provider" value manually` });
|
|
191
|
+
this.checkProviderEntitlement(data["provider"]);
|
|
182
192
|
data["auth_data"] = null;
|
|
183
193
|
}
|
|
184
194
|
if (data["external_identifier"] !== void 0) {
|
|
@@ -55,7 +55,9 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
55
55
|
knex: this.knex,
|
|
56
56
|
schema: this.schema
|
|
57
57
|
}).readOne(data["collection"])).meta?.versioning) throw new UnprocessableContentError({ reason: `Content Versioning is not enabled for collection "${data["collection"]}"` });
|
|
58
|
-
|
|
58
|
+
const isSingleton = !!this.schema.collections[data["collection"]]?.singleton;
|
|
59
|
+
if (itemLess) if (isSingleton) await this.assertSingletonEmpty(data["collection"]);
|
|
60
|
+
else return;
|
|
59
61
|
if ((await new VersionsService({
|
|
60
62
|
knex: this.knex,
|
|
61
63
|
schema: this.schema
|
|
@@ -64,9 +66,9 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
64
66
|
filter: {
|
|
65
67
|
key: { _eq: data["key"] },
|
|
66
68
|
collection: { _eq: data["collection"] },
|
|
67
|
-
item: { _eq: data["item"] }
|
|
69
|
+
item: itemLess ? { _null: true } : { _eq: data["item"] }
|
|
68
70
|
}
|
|
69
|
-
}))[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"]}"` });
|
|
70
72
|
}
|
|
71
73
|
async getMainItem(collection, item, query) {
|
|
72
74
|
return await new ItemsService(collection, {
|
|
@@ -114,6 +116,7 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
114
116
|
if (!Array.isArray(data)) throw new InvalidPayloadError({ reason: "Input should be an array of items" });
|
|
115
117
|
const keyCombos = /* @__PURE__ */ new Set();
|
|
116
118
|
for (const item of data) {
|
|
119
|
+
if (isNil(item["item"])) continue;
|
|
117
120
|
const keyCombo = `${item["key"]}-${item["collection"]}-${item["item"]}`;
|
|
118
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"]}"` });
|
|
119
122
|
keyCombos.add(keyCombo);
|
|
@@ -139,7 +142,20 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
139
142
|
const item = "item" in data ? data["item"] : existingVersion.item;
|
|
140
143
|
const key = "key" in data ? data["key"] : existingVersion.key;
|
|
141
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` });
|
|
142
|
-
if (item === null)
|
|
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;
|
|
158
|
+
}
|
|
143
159
|
const keyCombo = `${key}-${collection}-${item}`;
|
|
144
160
|
if (keyCombos.has(keyCombo)) throw new UnprocessableContentError({ reason: `Cannot update multiple versions on "${item}" in collection "${collection}" to the same key "${key}"` });
|
|
145
161
|
keyCombos.add(keyCombo);
|
|
@@ -282,6 +298,7 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
282
298
|
let updatedItemKey;
|
|
283
299
|
if (item) updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks, { overwriteDefaults: defaultOverwrites });
|
|
284
300
|
else {
|
|
301
|
+
await this.assertSingletonEmpty(collection);
|
|
285
302
|
updatedItemKey = await itemsService.createOne(payloadAfterHooks, { overwriteDefaults: defaultOverwrites });
|
|
286
303
|
await this.updateOne(version, { item: String(updatedItemKey) });
|
|
287
304
|
}
|
|
@@ -297,6 +314,11 @@ var VersionsService = class VersionsService extends ItemsService {
|
|
|
297
314
|
});
|
|
298
315
|
return updatedItemKey;
|
|
299
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
|
+
}
|
|
300
322
|
mapDelta(version) {
|
|
301
323
|
const delta = version.delta ?? {};
|
|
302
324
|
delta[this.schema.collections[version.collection].primary] = version.item;
|
|
@@ -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,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 };
|
|
@@ -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);
|
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({
|
|
@@ -144,7 +144,8 @@ async function handleVersion(self, key, query, opts) {
|
|
|
144
144
|
return result;
|
|
145
145
|
});
|
|
146
146
|
const env = useEnv();
|
|
147
|
-
|
|
147
|
+
const effectiveLimit = query.limit ?? Number(env["QUERY_LIMIT_DEFAULT"]);
|
|
148
|
+
if (effectiveLimit === -1 || results.length < effectiveLimit) results.push(...itemlessErrors.map((errorMeta) => {
|
|
148
149
|
let item = { $meta: errorMeta };
|
|
149
150
|
if (errorMeta.error) item = Object.assign({}, defaultItem, item, pick(errorMeta.delta, requestedFields));
|
|
150
151
|
return item;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "36.0.0-rc.
|
|
3
|
+
"version": "36.0.0-rc.1",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -166,30 +166,30 @@
|
|
|
166
166
|
"ws": "8.18.3",
|
|
167
167
|
"zod": "4.1.12",
|
|
168
168
|
"zod-validation-error": "4.0.2",
|
|
169
|
-
"@directus/app": "16.0.0-rc.
|
|
170
|
-
"@directus/
|
|
171
|
-
"@directus/constants": "14.4.0-rc.0",
|
|
172
|
-
"@directus/ai": "1.3.2-rc.0",
|
|
173
|
-
"@directus/extensions": "4.0.0-rc.0",
|
|
174
|
-
"@directus/extensions-registry": "4.0.0-rc.0",
|
|
169
|
+
"@directus/app": "16.0.0-rc.1",
|
|
170
|
+
"@directus/constants": "14.4.0-rc.1",
|
|
175
171
|
"@directus/errors": "2.4.0-rc.0",
|
|
176
|
-
"@directus/
|
|
177
|
-
"@directus/
|
|
172
|
+
"@directus/ai": "1.3.2-rc.0",
|
|
173
|
+
"@directus/env": "6.0.0-rc.1",
|
|
174
|
+
"@directus/extensions": "4.0.0-rc.1",
|
|
178
175
|
"@directus/format-title": "13.0.0-rc.0",
|
|
179
|
-
"@directus/
|
|
176
|
+
"@directus/extensions-registry": "4.0.0-rc.1",
|
|
177
|
+
"@directus/memory": "4.0.0-rc.1",
|
|
178
|
+
"@directus/pressure": "4.0.0-rc.1",
|
|
179
|
+
"@directus/extensions-sdk": "18.0.0-rc.1",
|
|
180
180
|
"@directus/schema": "14.0.0-rc.0",
|
|
181
181
|
"@directus/storage": "13.0.0-rc.0",
|
|
182
182
|
"@directus/specs": "14.0.0-rc.0",
|
|
183
|
-
"@directus/storage-driver-azure": "13.0.0-rc.
|
|
184
|
-
"@directus/storage-driver-cloudinary": "13.0.0-rc.
|
|
185
|
-
"@directus/storage-driver-gcs": "13.0.0-rc.
|
|
186
|
-
"@directus/storage-driver-supabase": "4.0.0-rc.0",
|
|
187
|
-
"@directus/storage-driver-s3": "13.0.0-rc.0",
|
|
183
|
+
"@directus/storage-driver-azure": "13.0.0-rc.1",
|
|
184
|
+
"@directus/storage-driver-cloudinary": "13.0.0-rc.1",
|
|
185
|
+
"@directus/storage-driver-gcs": "13.0.0-rc.1",
|
|
188
186
|
"@directus/storage-driver-local": "13.0.0-rc.0",
|
|
189
|
-
"@directus/
|
|
190
|
-
"@directus/system-data": "4.5.0-rc.
|
|
191
|
-
"@directus/
|
|
192
|
-
"directus": "
|
|
187
|
+
"@directus/storage-driver-s3": "13.0.0-rc.1",
|
|
188
|
+
"@directus/system-data": "4.5.0-rc.1",
|
|
189
|
+
"@directus/utils": "13.5.0-rc.1",
|
|
190
|
+
"@directus/storage-driver-supabase": "4.0.0-rc.1",
|
|
191
|
+
"@directus/validation": "3.0.0-rc.1",
|
|
192
|
+
"directus": "12.0.0-rc.2"
|
|
193
193
|
},
|
|
194
194
|
"devDependencies": {
|
|
195
195
|
"@directus/tsconfig": "4.0.0",
|
|
@@ -231,8 +231,8 @@
|
|
|
231
231
|
"knex-mock-client": "3.0.2",
|
|
232
232
|
"typescript": "5.9.3",
|
|
233
233
|
"vitest": "3.2.4",
|
|
234
|
-
"@directus/schema-builder": "1.0.0-rc.
|
|
235
|
-
"@directus/types": "16.0.0-rc.
|
|
234
|
+
"@directus/schema-builder": "1.0.0-rc.1",
|
|
235
|
+
"@directus/types": "16.0.0-rc.1"
|
|
236
236
|
},
|
|
237
237
|
"optionalDependencies": {
|
|
238
238
|
"@keyv/redis": "3.0.1",
|