@directus/api 33.0.0 → 33.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/chat/controllers/chat.post.js +19 -4
- package/dist/ai/chat/lib/create-ui-stream.d.ts +7 -6
- package/dist/ai/chat/lib/create-ui-stream.js +28 -25
- package/dist/ai/chat/middleware/load-settings.js +31 -7
- package/dist/ai/chat/models/chat-request.d.ts +135 -2
- package/dist/ai/chat/models/chat-request.js +56 -2
- package/dist/ai/chat/models/providers.d.ts +16 -2
- package/dist/ai/chat/models/providers.js +16 -2
- package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +3 -4
- package/dist/ai/chat/utils/format-context.d.ts +5 -0
- package/dist/ai/chat/utils/format-context.js +122 -0
- package/dist/ai/mcp/server.d.ts +27 -1
- package/dist/ai/providers/index.d.ts +3 -0
- package/dist/ai/providers/index.js +3 -0
- package/dist/ai/providers/options.d.ts +14 -0
- package/dist/ai/providers/options.js +26 -0
- package/dist/ai/providers/registry.d.ts +6 -0
- package/dist/ai/providers/registry.js +65 -0
- package/dist/ai/providers/types.d.ts +34 -0
- package/dist/ai/providers/types.js +1 -0
- package/dist/ai/tools/items/index.js +4 -1
- package/dist/ai/tools/items/prompt.md +7 -9
- package/dist/ai/tools/schema.js +1 -1
- package/dist/app.js +4 -0
- package/dist/auth/drivers/ldap.d.ts +1 -1
- package/dist/auth/drivers/ldap.js +142 -137
- package/dist/cache.d.ts +12 -0
- package/dist/cache.js +25 -1
- package/dist/cli/utils/create-env/env-stub.liquid +3 -0
- package/dist/controllers/deployment.d.ts +2 -0
- package/dist/controllers/deployment.js +481 -0
- package/dist/controllers/fields.js +6 -4
- package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
- package/dist/database/migrations/20260110A-add-ai-provider-settings.d.ts +3 -0
- package/dist/database/migrations/20260110A-add-ai-provider-settings.js +35 -0
- package/dist/database/migrations/20260128A-add-collaborative-editing.d.ts +3 -0
- package/dist/database/migrations/20260128A-add-collaborative-editing.js +10 -0
- package/dist/database/migrations/20260204A-add-deployment.d.ts +3 -0
- package/dist/database/migrations/20260204A-add-deployment.js +32 -0
- package/dist/database/run-ast/lib/apply-query/add-join.js +1 -1
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/index.js +1 -1
- package/dist/database/run-ast/lib/apply-query/sort.js +1 -1
- package/dist/deployment/deployment.d.ts +94 -0
- package/dist/deployment/deployment.js +29 -0
- package/dist/deployment/drivers/index.d.ts +1 -0
- package/dist/deployment/drivers/index.js +1 -0
- package/dist/deployment/drivers/vercel.d.ts +32 -0
- package/dist/deployment/drivers/vercel.js +208 -0
- package/dist/deployment/index.d.ts +2 -0
- package/dist/deployment/index.js +2 -0
- package/dist/deployment.d.ts +24 -0
- package/dist/deployment.js +39 -0
- package/dist/middleware/respond.js +27 -14
- package/dist/permissions/modules/process-ast/utils/find-related-collection.js +1 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +1 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +19 -8
- package/dist/server.js +2 -1
- package/dist/services/deployment-projects.d.ts +20 -0
- package/dist/services/deployment-projects.js +34 -0
- package/dist/services/deployment-runs.d.ts +13 -0
- package/dist/services/deployment-runs.js +6 -0
- package/dist/services/deployment.d.ts +40 -0
- package/dist/services/deployment.js +202 -0
- package/dist/services/graphql/resolvers/system-admin.js +2 -3
- package/dist/services/graphql/utils/filter-replace-m2a.js +3 -4
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +3 -0
- package/dist/services/server.js +1 -0
- package/dist/services/specifications.js +2 -2
- package/dist/services/versions.js +1 -1
- package/dist/telemetry/lib/get-report.js +2 -0
- package/dist/telemetry/types/report.d.ts +8 -0
- package/dist/telemetry/utils/get-settings.d.ts +2 -0
- package/dist/telemetry/utils/get-settings.js +5 -0
- package/dist/utils/deep-map-response.d.ts +1 -1
- package/dist/utils/deep-map-response.js +1 -1
- package/dist/utils/get-column-path.js +1 -1
- package/dist/utils/get-service.js +7 -1
- package/dist/utils/is-field-allowed.d.ts +4 -0
- package/dist/utils/is-field-allowed.js +9 -0
- package/dist/utils/versioning/handle-version.js +1 -1
- package/dist/websocket/collab/calculate-cache-metadata.d.ts +9 -0
- package/dist/websocket/collab/calculate-cache-metadata.js +121 -0
- package/dist/websocket/collab/collab.d.ts +63 -0
- package/dist/websocket/collab/collab.js +481 -0
- package/dist/websocket/collab/constants.d.ts +1 -0
- package/dist/websocket/collab/constants.js +13 -0
- package/dist/websocket/collab/filter-to-fields.d.ts +2 -0
- package/dist/websocket/collab/filter-to-fields.js +11 -0
- package/dist/websocket/collab/messenger.d.ts +43 -0
- package/dist/websocket/collab/messenger.js +225 -0
- package/dist/websocket/collab/payload-permissions.d.ts +18 -0
- package/dist/websocket/collab/payload-permissions.js +158 -0
- package/dist/websocket/collab/permissions-cache.d.ts +52 -0
- package/dist/websocket/collab/permissions-cache.js +204 -0
- package/dist/websocket/collab/room.d.ts +125 -0
- package/dist/websocket/collab/room.js +593 -0
- package/dist/websocket/collab/store.d.ts +7 -0
- package/dist/websocket/collab/store.js +33 -0
- package/dist/websocket/collab/types.d.ts +21 -0
- package/dist/websocket/collab/types.js +1 -0
- package/dist/websocket/collab/verify-permissions.d.ts +11 -0
- package/dist/websocket/collab/verify-permissions.js +100 -0
- package/dist/websocket/handlers/index.d.ts +2 -0
- package/dist/websocket/handlers/index.js +9 -0
- package/dist/websocket/utils/items.d.ts +2 -2
- package/dist/websocket/utils/message.d.ts +1 -1
- package/dist/websocket/utils/message.js +2 -2
- package/package.json +32 -30
- package/dist/utils/get-relation-info.d.ts +0 -6
- package/dist/utils/get-relation-info.js +0 -43
- package/dist/utils/get-relation-type.d.ts +0 -6
- package/dist/utils/get-relation-type.js +0 -18
- package/dist/utils/versioning/deep-map-with-schema.d.ts +0 -23
- package/dist/utils/versioning/deep-map-with-schema.js +0 -81
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AbstractServiceOptions, CachedResult, DeploymentConfig, PrimaryKey, Project, ProviderType, Query } from '@directus/types';
|
|
2
|
+
import type { DeploymentDriver } from '../deployment/deployment.js';
|
|
3
|
+
import { ItemsService } from './items.js';
|
|
4
|
+
export declare class DeploymentService extends ItemsService<DeploymentConfig> {
|
|
5
|
+
constructor(options: AbstractServiceOptions);
|
|
6
|
+
createOne(data: Partial<DeploymentConfig>, opts?: any): Promise<PrimaryKey>;
|
|
7
|
+
updateOne(key: PrimaryKey, data: Partial<DeploymentConfig>, opts?: any): Promise<PrimaryKey>;
|
|
8
|
+
/**
|
|
9
|
+
* Read deployment config by provider
|
|
10
|
+
*/
|
|
11
|
+
readByProvider(provider: ProviderType, query?: Query): Promise<DeploymentConfig>;
|
|
12
|
+
/**
|
|
13
|
+
* Update deployment config by provider
|
|
14
|
+
*/
|
|
15
|
+
updateByProvider(provider: ProviderType, data: Partial<DeploymentConfig>): Promise<PrimaryKey>;
|
|
16
|
+
/**
|
|
17
|
+
* Delete deployment config by provider
|
|
18
|
+
*/
|
|
19
|
+
deleteByProvider(provider: ProviderType): Promise<PrimaryKey>;
|
|
20
|
+
/**
|
|
21
|
+
* Read deployment config with decrypted credentials (internal use)
|
|
22
|
+
*/
|
|
23
|
+
private readConfig;
|
|
24
|
+
/**
|
|
25
|
+
* Parse JSON string or return value as-is
|
|
26
|
+
*/
|
|
27
|
+
private parseValue;
|
|
28
|
+
/**
|
|
29
|
+
* Get a deployment driver instance with decrypted credentials
|
|
30
|
+
*/
|
|
31
|
+
getDriver(provider: ProviderType): Promise<DeploymentDriver>;
|
|
32
|
+
/**
|
|
33
|
+
* List projects from provider with caching
|
|
34
|
+
*/
|
|
35
|
+
listProviderProjects(provider: ProviderType): Promise<CachedResult<Project[]>>;
|
|
36
|
+
/**
|
|
37
|
+
* Get project details from provider with caching
|
|
38
|
+
*/
|
|
39
|
+
getProviderProject(provider: ProviderType, projectId: string): Promise<CachedResult<Project>>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { InvalidPayloadError, InvalidProviderConfigError } from '@directus/errors';
|
|
3
|
+
import { mergeFilters, parseJSON } from '@directus/utils';
|
|
4
|
+
import { has, isEmpty } from 'lodash-es';
|
|
5
|
+
import { getCache, getCacheValueWithTTL, setCacheValueWithExpiry } from '../cache.js';
|
|
6
|
+
import { getDeploymentDriver } from '../deployment.js';
|
|
7
|
+
import { getMilliseconds } from '../utils/get-milliseconds.js';
|
|
8
|
+
import { ItemsService } from './items.js';
|
|
9
|
+
const env = useEnv();
|
|
10
|
+
const DEPLOYMENT_CACHE_TTL = getMilliseconds(env['CACHE_DEPLOYMENT_TTL']) || 5000; // Default 5s
|
|
11
|
+
export class DeploymentService extends ItemsService {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super('directus_deployments', options);
|
|
14
|
+
}
|
|
15
|
+
async createOne(data, opts) {
|
|
16
|
+
const provider = data.provider;
|
|
17
|
+
if (!provider) {
|
|
18
|
+
throw new InvalidPayloadError({ reason: 'Provider is required' });
|
|
19
|
+
}
|
|
20
|
+
if (isEmpty(data.credentials)) {
|
|
21
|
+
throw new InvalidPayloadError({ reason: 'Credentials are required' });
|
|
22
|
+
}
|
|
23
|
+
let credentials;
|
|
24
|
+
try {
|
|
25
|
+
credentials = this.parseValue(data.credentials, {});
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new InvalidPayloadError({ reason: 'Credentials must be valid JSON' });
|
|
29
|
+
}
|
|
30
|
+
let options;
|
|
31
|
+
try {
|
|
32
|
+
options = this.parseValue(data.options, undefined);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new InvalidPayloadError({ reason: 'Options must be valid JSON' });
|
|
36
|
+
}
|
|
37
|
+
// Test connection before persisting
|
|
38
|
+
const driver = getDeploymentDriver(provider, credentials, options);
|
|
39
|
+
try {
|
|
40
|
+
await driver.testConnection();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new InvalidProviderConfigError({ provider, reason: 'Invalid config connection' });
|
|
44
|
+
}
|
|
45
|
+
const payload = {
|
|
46
|
+
...data,
|
|
47
|
+
// Persist as string so payload service encrypts the value
|
|
48
|
+
credentials: JSON.stringify(credentials),
|
|
49
|
+
};
|
|
50
|
+
if (!isEmpty(options)) {
|
|
51
|
+
payload.options = JSON.stringify(options);
|
|
52
|
+
}
|
|
53
|
+
return super.createOne(payload, opts);
|
|
54
|
+
}
|
|
55
|
+
async updateOne(key, data, opts) {
|
|
56
|
+
const hasCredentials = has(data, 'credentials');
|
|
57
|
+
const hasOptions = has(data, 'options');
|
|
58
|
+
if (!hasCredentials && !hasOptions) {
|
|
59
|
+
return super.updateOne(key, data, opts);
|
|
60
|
+
}
|
|
61
|
+
const existing = await this.readOne(key);
|
|
62
|
+
const provider = existing.provider;
|
|
63
|
+
const internal = await this.readConfig(provider);
|
|
64
|
+
let credentials = this.parseValue(internal.credentials, {});
|
|
65
|
+
if (hasCredentials) {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = this.parseValue(data.credentials, {});
|
|
68
|
+
credentials = { ...credentials, ...parsed };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new InvalidPayloadError({ reason: 'Credentials must be valid JSON or object' });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
let options = existing.options ?? undefined;
|
|
75
|
+
if (hasOptions) {
|
|
76
|
+
try {
|
|
77
|
+
options = this.parseValue(data.options, undefined);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new InvalidPayloadError({ reason: 'Options must be valid JSON' });
|
|
81
|
+
}
|
|
82
|
+
if (isEmpty(options)) {
|
|
83
|
+
throw new InvalidPayloadError({ reason: 'Options must not be empty' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Test connection before persisting
|
|
87
|
+
const driver = getDeploymentDriver(provider, credentials, options);
|
|
88
|
+
try {
|
|
89
|
+
await driver.testConnection();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new InvalidProviderConfigError({ provider, reason: 'Invalid config connection' });
|
|
93
|
+
}
|
|
94
|
+
return super.updateOne(key, {
|
|
95
|
+
credentials: JSON.stringify(credentials),
|
|
96
|
+
...(!isEmpty(options) ? { options: JSON.stringify(options) } : {}),
|
|
97
|
+
}, opts);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Read deployment config by provider
|
|
101
|
+
*/
|
|
102
|
+
async readByProvider(provider, query) {
|
|
103
|
+
const results = await this.readByQuery({
|
|
104
|
+
...query,
|
|
105
|
+
filter: mergeFilters({ provider: { _eq: provider } }, query?.filter ?? null),
|
|
106
|
+
limit: 1,
|
|
107
|
+
});
|
|
108
|
+
if (!results || results.length === 0) {
|
|
109
|
+
throw new Error(`Deployment config for "${provider}" not found`);
|
|
110
|
+
}
|
|
111
|
+
return results[0];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Update deployment config by provider
|
|
115
|
+
*/
|
|
116
|
+
async updateByProvider(provider, data) {
|
|
117
|
+
const deployment = await this.readByProvider(provider);
|
|
118
|
+
return this.updateOne(deployment.id, data);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Delete deployment config by provider
|
|
122
|
+
*/
|
|
123
|
+
async deleteByProvider(provider) {
|
|
124
|
+
const deployment = await this.readByProvider(provider);
|
|
125
|
+
return this.deleteOne(deployment.id);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Read deployment config with decrypted credentials (internal use)
|
|
129
|
+
*/
|
|
130
|
+
async readConfig(provider) {
|
|
131
|
+
const internalService = new ItemsService('directus_deployments', {
|
|
132
|
+
knex: this.knex,
|
|
133
|
+
schema: this.schema,
|
|
134
|
+
accountability: null,
|
|
135
|
+
});
|
|
136
|
+
const results = await internalService.readByQuery({
|
|
137
|
+
filter: { provider: { _eq: provider } },
|
|
138
|
+
limit: 1,
|
|
139
|
+
});
|
|
140
|
+
if (!results || results.length === 0) {
|
|
141
|
+
throw new Error(`Deployment config for "${provider}" not found`);
|
|
142
|
+
}
|
|
143
|
+
return results[0];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Parse JSON string or return value as-is
|
|
147
|
+
*/
|
|
148
|
+
parseValue(value, fallback) {
|
|
149
|
+
if (!value)
|
|
150
|
+
return fallback;
|
|
151
|
+
if (typeof value === 'string')
|
|
152
|
+
return parseJSON(value);
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get a deployment driver instance with decrypted credentials
|
|
157
|
+
*/
|
|
158
|
+
async getDriver(provider) {
|
|
159
|
+
const deployment = await this.readConfig(provider);
|
|
160
|
+
const credentials = this.parseValue(deployment.credentials, {});
|
|
161
|
+
const options = this.parseValue(deployment.options, {});
|
|
162
|
+
return getDeploymentDriver(deployment.provider, credentials, options);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* List projects from provider with caching
|
|
166
|
+
*/
|
|
167
|
+
async listProviderProjects(provider) {
|
|
168
|
+
const cacheKey = `${provider}:projects`;
|
|
169
|
+
const { deploymentCache } = getCache();
|
|
170
|
+
// Check cache first
|
|
171
|
+
const cached = await getCacheValueWithTTL(deploymentCache, cacheKey);
|
|
172
|
+
if (cached) {
|
|
173
|
+
return { data: cached.data, remainingTTL: cached.remainingTTL };
|
|
174
|
+
}
|
|
175
|
+
// Fetch from driver
|
|
176
|
+
const driver = await this.getDriver(provider);
|
|
177
|
+
const projects = await driver.listProjects();
|
|
178
|
+
// Store in cache
|
|
179
|
+
await setCacheValueWithExpiry(deploymentCache, cacheKey, projects, DEPLOYMENT_CACHE_TTL);
|
|
180
|
+
// Return with full TTL (just cached)
|
|
181
|
+
return { data: projects, remainingTTL: DEPLOYMENT_CACHE_TTL };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get project details from provider with caching
|
|
185
|
+
*/
|
|
186
|
+
async getProviderProject(provider, projectId) {
|
|
187
|
+
const cacheKey = `${provider}:project:${projectId}`;
|
|
188
|
+
const { deploymentCache } = getCache();
|
|
189
|
+
// Check cache first
|
|
190
|
+
const cached = await getCacheValueWithTTL(deploymentCache, cacheKey);
|
|
191
|
+
if (cached) {
|
|
192
|
+
return { data: cached.data, remainingTTL: cached.remainingTTL };
|
|
193
|
+
}
|
|
194
|
+
// Fetch from driver
|
|
195
|
+
const driver = await this.getDriver(provider);
|
|
196
|
+
const project = await driver.getProject(projectId);
|
|
197
|
+
// Store in cache
|
|
198
|
+
await setCacheValueWithExpiry(deploymentCache, cacheKey, project, DEPLOYMENT_CACHE_TTL);
|
|
199
|
+
// Return with full TTL (just cached)
|
|
200
|
+
return { data: project, remainingTTL: DEPLOYMENT_CACHE_TTL };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -2,7 +2,6 @@ import { InvalidPayloadError } from '@directus/errors';
|
|
|
2
2
|
import { isSystemField } from '@directus/system-data';
|
|
3
3
|
import { GraphQLBoolean, GraphQLID, GraphQLList, GraphQLNonNull, GraphQLString } from 'graphql';
|
|
4
4
|
import { SchemaComposer, toInputObjectType } from 'graphql-compose';
|
|
5
|
-
import { fromZodError } from 'zod-validation-error';
|
|
6
5
|
import { CollectionsService } from '../../collections.js';
|
|
7
6
|
import { ExtensionsService } from '../../extensions.js';
|
|
8
7
|
import { FieldsService, systemFieldUpdateSchema } from '../../fields.js';
|
|
@@ -112,7 +111,7 @@ export function resolveSystemAdmin(gql, schema, schemaComposer) {
|
|
|
112
111
|
if (isSystemField(args['collection'], args['field'])) {
|
|
113
112
|
const validationResult = systemFieldUpdateSchema.safeParse(args['data']);
|
|
114
113
|
if (!validationResult.success) {
|
|
115
|
-
throw new InvalidPayloadError({ reason:
|
|
114
|
+
throw new InvalidPayloadError({ reason: 'Only "schema.is_indexed" may be modified for system fields' });
|
|
116
115
|
}
|
|
117
116
|
}
|
|
118
117
|
await service.updateField(args['collection'], {
|
|
@@ -140,7 +139,7 @@ export function resolveSystemAdmin(gql, schema, schemaComposer) {
|
|
|
140
139
|
if (isSystemField(args['collection'], fieldData['field'])) {
|
|
141
140
|
const validationResult = systemFieldUpdateSchema.safeParse(fieldData);
|
|
142
141
|
if (!validationResult.success) {
|
|
143
|
-
throw new InvalidPayloadError({ reason:
|
|
142
|
+
throw new InvalidPayloadError({ reason: 'Only "schema.is_indexed" may be modified for system fields' });
|
|
144
143
|
}
|
|
145
144
|
}
|
|
146
145
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { getRelation } from '@directus/utils';
|
|
2
|
-
import { getRelationType } from '../../../utils/get-relation-type.js';
|
|
1
|
+
import { getRelation, getRelationType } from '@directus/utils';
|
|
3
2
|
export function filterReplaceM2A(filter_arg, collection, schema, options) {
|
|
4
3
|
const filter = filter_arg;
|
|
5
4
|
for (const key in filter) {
|
|
@@ -10,7 +9,7 @@ export function filterReplaceM2A(filter_arg, collection, schema, options) {
|
|
|
10
9
|
continue;
|
|
11
10
|
field = options?.aliasMap?.[field] ?? field;
|
|
12
11
|
const relation = getRelation(schema.relations, collection, field);
|
|
13
|
-
const type = relation ? getRelationType({ relation, collection, field }) : null;
|
|
12
|
+
const type = relation ? getRelationType({ relation, collection, field, useA2O: true }) : null;
|
|
14
13
|
if (type === 'o2m' && relation) {
|
|
15
14
|
filter[key] = filterReplaceM2A(filter[key], relation.collection, schema, options);
|
|
16
15
|
}
|
|
@@ -46,7 +45,7 @@ export function filterReplaceM2ADeep(deep_arg, collection, schema, options) {
|
|
|
46
45
|
const relation = getRelation(schema.relations, collection, field);
|
|
47
46
|
if (!relation)
|
|
48
47
|
continue;
|
|
49
|
-
const type = getRelationType({ relation, collection, field });
|
|
48
|
+
const type = getRelationType({ relation, collection, field, useA2O: true });
|
|
50
49
|
if (type === 'o2m') {
|
|
51
50
|
deep[key] = filterReplaceM2ADeep(deep[key], relation.collection, schema);
|
|
52
51
|
}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export * from './fields.js';
|
|
|
10
10
|
export * from './files.js';
|
|
11
11
|
export * from './flows.js';
|
|
12
12
|
export * from './folders.js';
|
|
13
|
+
export * from './deployment.js';
|
|
14
|
+
export * from './deployment-projects.js';
|
|
15
|
+
export * from './deployment-runs.js';
|
|
13
16
|
export * from './graphql/index.js';
|
|
14
17
|
export * from './import-export.js';
|
|
15
18
|
export * from './items.js';
|
package/dist/services/index.js
CHANGED
|
@@ -10,6 +10,9 @@ export * from './fields.js';
|
|
|
10
10
|
export * from './files.js';
|
|
11
11
|
export * from './flows.js';
|
|
12
12
|
export * from './folders.js';
|
|
13
|
+
export * from './deployment.js';
|
|
14
|
+
export * from './deployment-projects.js';
|
|
15
|
+
export * from './deployment-runs.js';
|
|
13
16
|
export * from './graphql/index.js';
|
|
14
17
|
export * from './import-export.js';
|
|
15
18
|
export * from './items.js';
|
package/dist/services/server.js
CHANGED
|
@@ -103,6 +103,7 @@ export class ServerService {
|
|
|
103
103
|
info['websocket'].heartbeat = toBoolean(env['WEBSOCKETS_HEARTBEAT_ENABLED'])
|
|
104
104
|
? env['WEBSOCKETS_HEARTBEAT_PERIOD']
|
|
105
105
|
: false;
|
|
106
|
+
info['websocket'].collaborativeEditing = toBoolean(env['WEBSOCKETS_COLLAB_ENABLED']);
|
|
106
107
|
info['websocket'].logs =
|
|
107
108
|
toBoolean(env['WEBSOCKETS_LOGS_ENABLED']) && this.accountability.admin
|
|
108
109
|
? {
|
|
@@ -2,7 +2,7 @@ import { useEnv } from '@directus/env';
|
|
|
2
2
|
import formatTitle from '@directus/format-title';
|
|
3
3
|
import { spec } from '@directus/specs';
|
|
4
4
|
import { isSystemCollection } from '@directus/system-data';
|
|
5
|
-
import { getRelation } from '@directus/utils';
|
|
5
|
+
import { getRelation, getRelationType } from '@directus/utils';
|
|
6
6
|
import { cloneDeep, mergeWith } from 'lodash-es';
|
|
7
7
|
import hash from 'object-hash';
|
|
8
8
|
import { OAS_REQUIRED_SCHEMAS } from '../constants.js';
|
|
@@ -10,7 +10,6 @@ import getDatabase from '../database/index.js';
|
|
|
10
10
|
import { fetchPermissions } from '../permissions/lib/fetch-permissions.js';
|
|
11
11
|
import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
|
|
12
12
|
import { fetchAllowedFieldMap } from '../permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js';
|
|
13
|
-
import { getRelationType } from '../utils/get-relation-type.js';
|
|
14
13
|
import { reduceSchema } from '../utils/reduce-schema.js';
|
|
15
14
|
import { GraphQLService } from './graphql/index.js';
|
|
16
15
|
const env = useEnv();
|
|
@@ -362,6 +361,7 @@ class OASSpecsService {
|
|
|
362
361
|
relation,
|
|
363
362
|
field: field.field,
|
|
364
363
|
collection: collection,
|
|
364
|
+
useA2O: true,
|
|
365
365
|
});
|
|
366
366
|
if (relationType === 'm2o') {
|
|
367
367
|
const relatedTag = tags.find((tag) => tag['x-collection'] === relation.related_collection);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Action } from '@directus/constants';
|
|
2
2
|
import { ForbiddenError, InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
|
|
3
|
+
import { deepMapWithSchema } from '@directus/utils';
|
|
3
4
|
import Joi from 'joi';
|
|
4
5
|
import { assign, get, isEqual, isPlainObject, pick } from 'lodash-es';
|
|
5
6
|
import objectHash from 'object-hash';
|
|
@@ -8,7 +9,6 @@ import { getHelpers } from '../database/helpers/index.js';
|
|
|
8
9
|
import emitter from '../emitter.js';
|
|
9
10
|
import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
|
|
10
11
|
import { shouldClearCache } from '../utils/should-clear-cache.js';
|
|
11
|
-
import { deepMapWithSchema } from '../utils/versioning/deep-map-with-schema.js';
|
|
12
12
|
import { splitRecursive } from '../utils/versioning/split-recursive.js';
|
|
13
13
|
import { ActivityService } from './activity.js';
|
|
14
14
|
import { ItemsService } from './items.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
+
import { toBoolean } from '@directus/utils';
|
|
2
3
|
import { version } from 'directus/version';
|
|
3
4
|
import { getHelpers } from '../../database/helpers/index.js';
|
|
4
5
|
import { getDatabase, getDatabaseClient } from '../../database/index.js';
|
|
@@ -55,6 +56,7 @@ export const getReport = async () => {
|
|
|
55
56
|
extensions: extensionsCounts.totalEnabled,
|
|
56
57
|
database_size: databaseSize ?? 0,
|
|
57
58
|
files_size_total: filesizes.total,
|
|
59
|
+
websockets_enabled: toBoolean(env['WEBSOCKETS_ENABLED'] ?? false),
|
|
58
60
|
...settings,
|
|
59
61
|
};
|
|
60
62
|
};
|
|
@@ -91,4 +91,12 @@ export interface TelemetryReport {
|
|
|
91
91
|
* Number of Visual Editor URLs configured in the system
|
|
92
92
|
*/
|
|
93
93
|
visual_editor_urls: number;
|
|
94
|
+
/**
|
|
95
|
+
* Whether collaborative editing is enabled
|
|
96
|
+
*/
|
|
97
|
+
collaborative_editing_enabled: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Whether WebSockets are enabled
|
|
100
|
+
*/
|
|
101
|
+
websockets_enabled: boolean;
|
|
94
102
|
}
|
|
@@ -8,6 +8,7 @@ export type TelemetrySettings = {
|
|
|
8
8
|
ai_openai_api_key: boolean;
|
|
9
9
|
ai_anthropic_api_key: boolean;
|
|
10
10
|
ai_system_prompt: boolean;
|
|
11
|
+
collaborative_editing_enabled: boolean;
|
|
11
12
|
};
|
|
12
13
|
export type DatabaseSettings = {
|
|
13
14
|
project_id: string;
|
|
@@ -20,5 +21,6 @@ export type DatabaseSettings = {
|
|
|
20
21
|
ai_openai_api_key?: string;
|
|
21
22
|
ai_anthropic_api_key?: string;
|
|
22
23
|
ai_system_prompt?: string;
|
|
24
|
+
collaborative_editing_enabled?: boolean;
|
|
23
25
|
};
|
|
24
26
|
export declare const getSettings: (db: Knex) => Promise<TelemetrySettings>;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { toBoolean } from '@directus/utils';
|
|
1
3
|
import { SettingsService } from '../../services/settings.js';
|
|
2
4
|
import { getSchema } from '../../utils/get-schema.js';
|
|
3
5
|
export const getSettings = async (db) => {
|
|
6
|
+
const env = useEnv();
|
|
4
7
|
const settingsService = new SettingsService({
|
|
5
8
|
knex: db,
|
|
6
9
|
schema: await getSchema({ database: db }),
|
|
@@ -15,6 +18,7 @@ export const getSettings = async (db) => {
|
|
|
15
18
|
'ai_openai_api_key',
|
|
16
19
|
'ai_anthropic_api_key',
|
|
17
20
|
'ai_system_prompt',
|
|
21
|
+
'collaborative_editing_enabled',
|
|
18
22
|
],
|
|
19
23
|
}));
|
|
20
24
|
return {
|
|
@@ -26,5 +30,6 @@ export const getSettings = async (db) => {
|
|
|
26
30
|
ai_openai_api_key: Boolean(settings?.ai_openai_api_key),
|
|
27
31
|
ai_anthropic_api_key: Boolean(settings?.ai_anthropic_api_key),
|
|
28
32
|
ai_system_prompt: Boolean(settings?.ai_system_prompt),
|
|
33
|
+
collaborative_editing_enabled: toBoolean(env['WEBSOCKETS_COLLAB_ENABLED'] ?? true) && (settings?.collaborative_editing_enabled ?? false),
|
|
29
34
|
};
|
|
30
35
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CollectionOverview, FieldOverview, Relation, SchemaOverview } from '@directus/types';
|
|
2
|
-
import { type RelationInfo } from '
|
|
2
|
+
import { type RelationInfo } from '@directus/utils';
|
|
3
3
|
/**
|
|
4
4
|
* Allows to deep map the response from the ItemsService with collection, field and relation context for each entry.
|
|
5
5
|
* Bottom to Top depth first mapping of values.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { InvalidQueryError } from '@directus/errors';
|
|
3
|
+
import { getRelationInfo } from '@directus/utils';
|
|
3
4
|
import { isPlainObject } from 'lodash-es';
|
|
4
|
-
import { getRelationInfo } from './get-relation-info.js';
|
|
5
5
|
/**
|
|
6
6
|
* Allows to deep map the response from the ItemsService with collection, field and relation context for each entry.
|
|
7
7
|
* Bottom to Top depth first mapping of values.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { InvalidQueryError } from '@directus/errors';
|
|
2
|
-
import { getRelationInfo } from '
|
|
2
|
+
import { getRelationInfo } from '@directus/utils';
|
|
3
3
|
/**
|
|
4
4
|
* Converts a Directus field list path to the correct SQL names based on the constructed alias map.
|
|
5
5
|
* For example: ['author', 'role', 'name'] -> 'ljnsv.name'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ForbiddenError } from '@directus/errors';
|
|
2
|
-
import { AccessService, ActivityService, CommentsService, DashboardsService, FilesService, FlowsService, FoldersService, ItemsService, NotificationsService, OperationsService, PanelsService, PermissionsService, PoliciesService, PresetsService, RevisionsService, RolesService, SettingsService, SharesService, TranslationsService, UsersService, VersionsService, } from '../services/index.js';
|
|
2
|
+
import { AccessService, ActivityService, CommentsService, DashboardsService, DeploymentProjectsService, DeploymentRunsService, DeploymentService, FilesService, FlowsService, FoldersService, ItemsService, NotificationsService, OperationsService, PanelsService, PermissionsService, PoliciesService, PresetsService, RevisionsService, RolesService, SettingsService, SharesService, TranslationsService, UsersService, VersionsService, } from '../services/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Select the correct service for the given collection. This allows the individual services to run
|
|
5
5
|
* their custom checks (f.e. it allows `UsersService` to prevent updating TFA secret from outside).
|
|
@@ -46,6 +46,12 @@ export function getService(collection, opts) {
|
|
|
46
46
|
return new UsersService(opts);
|
|
47
47
|
case 'directus_versions':
|
|
48
48
|
return new VersionsService(opts);
|
|
49
|
+
case 'directus_deployments':
|
|
50
|
+
return new DeploymentService(opts);
|
|
51
|
+
case 'directus_deployment_projects':
|
|
52
|
+
return new DeploymentProjectsService(opts);
|
|
53
|
+
case 'directus_deployment_runs':
|
|
54
|
+
return new DeploymentRunsService(opts);
|
|
49
55
|
default:
|
|
50
56
|
// Deny usage of other system collections via ItemsService
|
|
51
57
|
if (collection.startsWith('directus_'))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a specific field is allowed within a set of allowed fields
|
|
3
|
+
*/
|
|
4
|
+
export function isFieldAllowed(allowedFields, field) {
|
|
5
|
+
if (Array.isArray(allowedFields)) {
|
|
6
|
+
return allowedFields.includes(field) || allowedFields.includes('*');
|
|
7
|
+
}
|
|
8
|
+
return allowedFields.has(field) || allowedFields.has('*');
|
|
9
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ForbiddenError } from '@directus/errors';
|
|
2
|
+
import { deepMapWithSchema } from '@directus/utils';
|
|
2
3
|
import { transaction } from '../transaction.js';
|
|
3
|
-
import { deepMapWithSchema } from './deep-map-with-schema.js';
|
|
4
4
|
import { splitRecursive } from './split-recursive.js';
|
|
5
5
|
export async function handleVersion(self, key, queryWithKey, opts) {
|
|
6
6
|
const { VersionsService } = await import('../../services/versions.js');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Accountability, SchemaOverview } from '@directus/types';
|
|
2
|
+
export declare const DYNAMIC_VARIABLE_MAP: Record<string, string>;
|
|
3
|
+
/**
|
|
4
|
+
* Calculate logical expiry (TTL) and dependencies for permissions caching.
|
|
5
|
+
*/
|
|
6
|
+
export declare function calculateCacheMetadata(collection: string, itemData: any, rawPermissions: any[], schema: SchemaOverview, accountability: Accountability): {
|
|
7
|
+
ttlMs: number | undefined;
|
|
8
|
+
dependencies: string[];
|
|
9
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { REGEX_BETWEEN_PARENS } from '@directus/constants';
|
|
2
|
+
import { adjustDate } from '@directus/utils';
|
|
3
|
+
export const DYNAMIC_VARIABLE_MAP = {
|
|
4
|
+
$CURRENT_USER: 'directus_users',
|
|
5
|
+
$CURRENT_ROLE: 'directus_roles',
|
|
6
|
+
$CURRENT_ROLES: 'directus_roles',
|
|
7
|
+
$CURRENT_POLICIES: 'directus_policies',
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Calculate logical expiry (TTL) and dependencies for permissions caching.
|
|
11
|
+
*/
|
|
12
|
+
export function calculateCacheMetadata(collection, itemData, rawPermissions, schema, accountability) {
|
|
13
|
+
let ttlMs;
|
|
14
|
+
const dependencies = new Set();
|
|
15
|
+
if (itemData) {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
let closestExpiry = Infinity;
|
|
18
|
+
// Scan permission filters for dynamic variables and relational dependencies to determine cache invalidation rules
|
|
19
|
+
const scan = (val, fieldKey, currentCollection = collection) => {
|
|
20
|
+
if (!val || typeof val !== 'object')
|
|
21
|
+
return;
|
|
22
|
+
for (const [key, value] of Object.entries(val)) {
|
|
23
|
+
// Parse dynamic variables
|
|
24
|
+
if (typeof value === 'string' && value.startsWith('$')) {
|
|
25
|
+
// $NOW requires calculating a logical expiry (TTL) based on the field value
|
|
26
|
+
if (value.startsWith('$NOW')) {
|
|
27
|
+
const field = fieldKey || key;
|
|
28
|
+
const dateValue = itemData[field];
|
|
29
|
+
if (dateValue) {
|
|
30
|
+
let ruleDate = new Date();
|
|
31
|
+
if (value.includes('(')) {
|
|
32
|
+
const adjustment = value.match(REGEX_BETWEEN_PARENS)?.[1];
|
|
33
|
+
if (adjustment)
|
|
34
|
+
ruleDate = adjustDate(ruleDate, adjustment) || ruleDate;
|
|
35
|
+
}
|
|
36
|
+
const adjustmentMs = ruleDate.getTime() - now;
|
|
37
|
+
const expiry = new Date(dateValue).getTime() - adjustmentMs;
|
|
38
|
+
if (expiry > now && expiry < closestExpiry) {
|
|
39
|
+
closestExpiry = expiry;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Other dynamic variables ($CURRENT_USER, etc) create collection-based dependencies
|
|
45
|
+
const parts = value.split('.');
|
|
46
|
+
const dynamicVariable = parts[0];
|
|
47
|
+
const rootCollection = DYNAMIC_VARIABLE_MAP[dynamicVariable];
|
|
48
|
+
if (rootCollection) {
|
|
49
|
+
// Only $CURRENT_USER needs granular tagging
|
|
50
|
+
// Other dynamic variables trigger full cache wipe so collection-level is sufficient
|
|
51
|
+
if (dynamicVariable === '$CURRENT_USER' && accountability.user) {
|
|
52
|
+
dependencies.add(`${rootCollection}:${accountability.user}`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
dependencies.add(rootCollection);
|
|
56
|
+
}
|
|
57
|
+
// Track all intermediate collections in the path
|
|
58
|
+
if (parts.length > 1) {
|
|
59
|
+
let currentCollection = rootCollection;
|
|
60
|
+
for (const segment of parts.slice(1, -1)) {
|
|
61
|
+
if (!currentCollection)
|
|
62
|
+
break;
|
|
63
|
+
const relation = schema.relations.find((r) => (r.collection === currentCollection && r.field === segment) ||
|
|
64
|
+
(r.related_collection === currentCollection && r.meta?.one_field === segment));
|
|
65
|
+
if (relation) {
|
|
66
|
+
currentCollection =
|
|
67
|
+
relation.collection === currentCollection
|
|
68
|
+
? relation.related_collection
|
|
69
|
+
: relation.collection;
|
|
70
|
+
if (currentCollection)
|
|
71
|
+
dependencies.add(currentCollection);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
currentCollection = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Parse relational filter dependencies to track which collections affect this permission
|
|
82
|
+
let field = key;
|
|
83
|
+
if (key.includes('(') && key.includes(')')) {
|
|
84
|
+
const columnName = key.match(REGEX_BETWEEN_PARENS)?.[1];
|
|
85
|
+
if (columnName)
|
|
86
|
+
field = columnName;
|
|
87
|
+
}
|
|
88
|
+
if (!field.startsWith('_')) {
|
|
89
|
+
const relation = schema.relations.find((r) => (r.collection === currentCollection && r.field === field) ||
|
|
90
|
+
(r.related_collection === currentCollection && r.meta?.one_field === field));
|
|
91
|
+
let targetCol = null;
|
|
92
|
+
if (relation) {
|
|
93
|
+
targetCol = relation.collection === currentCollection ? relation.related_collection : relation.collection;
|
|
94
|
+
}
|
|
95
|
+
if (targetCol) {
|
|
96
|
+
dependencies.add(targetCol);
|
|
97
|
+
scan(value, undefined, targetCol);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Not a relation, but might be a nested filter object
|
|
101
|
+
scan(value, field.startsWith('_') ? fieldKey : field, currentCollection);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Keep scanning filter operators
|
|
106
|
+
scan(value, fieldKey, currentCollection);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
// Scan all raw permissions to collect dependencies and calculate TTL
|
|
111
|
+
for (const permission of rawPermissions) {
|
|
112
|
+
scan(permission.permissions);
|
|
113
|
+
}
|
|
114
|
+
if (closestExpiry !== Infinity) {
|
|
115
|
+
ttlMs = closestExpiry - now;
|
|
116
|
+
// Limit TTL to between 1s and 1 hour
|
|
117
|
+
ttlMs = Math.max(1000, Math.min(ttlMs, 3600000));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { ttlMs, dependencies: Array.from(dependencies) };
|
|
121
|
+
}
|