@directus/api 33.3.1 → 34.0.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/lib/create-ui-stream.js +2 -1
- package/dist/ai/chat/lib/transform-file-parts.d.ts +12 -0
- package/dist/ai/chat/lib/transform-file-parts.js +36 -0
- package/dist/ai/files/adapters/anthropic.d.ts +3 -0
- package/dist/ai/files/adapters/anthropic.js +25 -0
- package/dist/ai/files/adapters/google.d.ts +3 -0
- package/dist/ai/files/adapters/google.js +58 -0
- package/dist/ai/files/adapters/index.d.ts +3 -0
- package/dist/ai/files/adapters/index.js +3 -0
- package/dist/ai/files/adapters/openai.d.ts +3 -0
- package/dist/ai/files/adapters/openai.js +22 -0
- package/dist/ai/files/controllers/upload.d.ts +2 -0
- package/dist/ai/files/controllers/upload.js +101 -0
- package/dist/ai/files/lib/fetch-provider.d.ts +1 -0
- package/dist/ai/files/lib/fetch-provider.js +23 -0
- package/dist/ai/files/lib/upload-to-provider.d.ts +4 -0
- package/dist/ai/files/lib/upload-to-provider.js +26 -0
- package/dist/ai/files/router.d.ts +1 -0
- package/dist/ai/files/router.js +5 -0
- package/dist/ai/files/types.d.ts +5 -0
- package/dist/ai/files/types.js +1 -0
- package/dist/ai/providers/anthropic-file-support.d.ts +12 -0
- package/dist/ai/providers/anthropic-file-support.js +94 -0
- package/dist/ai/providers/registry.js +3 -6
- package/dist/ai/tools/flows/index.d.ts +16 -16
- package/dist/ai/tools/schema.d.ts +8 -8
- package/dist/ai/tools/schema.js +2 -2
- package/dist/app.js +10 -1
- package/dist/controllers/deployment-webhooks.d.ts +2 -0
- package/dist/controllers/deployment-webhooks.js +95 -0
- package/dist/controllers/deployment.js +61 -165
- package/dist/controllers/files.js +2 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +52 -26
- package/dist/database/helpers/date/dialects/oracle.js +2 -0
- package/dist/database/helpers/date/dialects/sqlite.js +2 -0
- package/dist/database/helpers/date/types.d.ts +1 -1
- package/dist/database/helpers/date/types.js +3 -1
- package/dist/database/helpers/fn/dialects/mssql.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/mssql.js +21 -0
- package/dist/database/helpers/fn/dialects/mysql.d.ts +2 -0
- package/dist/database/helpers/fn/dialects/mysql.js +30 -0
- package/dist/database/helpers/fn/dialects/oracle.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/oracle.js +21 -0
- package/dist/database/helpers/fn/dialects/postgres.d.ts +14 -0
- package/dist/database/helpers/fn/dialects/postgres.js +40 -0
- package/dist/database/helpers/fn/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/fn/dialects/sqlite.js +12 -0
- package/dist/database/helpers/fn/json/parse-function.d.ts +19 -0
- package/dist/database/helpers/fn/json/parse-function.js +66 -0
- package/dist/database/helpers/fn/types.d.ts +8 -0
- package/dist/database/helpers/fn/types.js +19 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/mysql.js +11 -0
- package/dist/database/helpers/schema/types.d.ts +1 -0
- package/dist/database/helpers/schema/types.js +3 -0
- package/dist/database/index.js +2 -1
- package/dist/database/migrations/20260211A-add-deployment-webhooks.d.ts +3 -0
- package/dist/database/migrations/20260211A-add-deployment-webhooks.js +37 -0
- 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/operator.js +17 -7
- package/dist/database/run-ast/lib/parse-current-level.js +8 -1
- package/dist/database/run-ast/run-ast.js +11 -1
- package/dist/database/run-ast/utils/apply-function-to-column-name.js +7 -1
- package/dist/database/run-ast/utils/get-column.js +13 -2
- package/dist/deployment/deployment.d.ts +25 -2
- package/dist/deployment/drivers/netlify.d.ts +6 -2
- package/dist/deployment/drivers/netlify.js +114 -12
- package/dist/deployment/drivers/vercel.d.ts +5 -2
- package/dist/deployment/drivers/vercel.js +84 -5
- package/dist/deployment.d.ts +5 -0
- package/dist/deployment.js +34 -0
- package/dist/permissions/utils/get-unaliased-field-key.js +9 -1
- package/dist/request/is-denied-ip.js +24 -23
- package/dist/services/authentication.js +27 -22
- package/dist/services/collections.js +1 -0
- package/dist/services/deployment-projects.d.ts +31 -2
- package/dist/services/deployment-projects.js +109 -5
- package/dist/services/deployment-runs.d.ts +19 -1
- package/dist/services/deployment-runs.js +86 -0
- package/dist/services/deployment.d.ts +44 -3
- package/dist/services/deployment.js +263 -15
- package/dist/services/files/utils/get-metadata.js +6 -6
- package/dist/services/files.d.ts +3 -1
- package/dist/services/files.js +26 -3
- package/dist/services/graphql/resolvers/query.js +23 -6
- package/dist/services/payload.d.ts +6 -0
- package/dist/services/payload.js +27 -2
- package/dist/services/server.js +1 -1
- package/dist/services/users.js +6 -1
- package/dist/utils/get-field-relational-depth.d.ts +13 -0
- package/dist/utils/get-field-relational-depth.js +22 -0
- package/dist/utils/parse-value.d.ts +4 -0
- package/dist/utils/parse-value.js +11 -0
- package/dist/utils/sanitize-query.js +3 -2
- package/dist/utils/split-fields.d.ts +4 -0
- package/dist/utils/split-fields.js +32 -0
- package/dist/utils/validate-query.js +2 -1
- package/package.json +29 -29
|
@@ -42,11 +42,25 @@ export class AuthenticationService {
|
|
|
42
42
|
const STALL_TIME = env['LOGIN_STALL_TIME'];
|
|
43
43
|
const timeStart = performance.now();
|
|
44
44
|
const provider = getAuthProvider(providerName);
|
|
45
|
+
const emitStatus = (status, loginPayload, loginUser, error) => {
|
|
46
|
+
emitter.emitAction('auth.login', {
|
|
47
|
+
payload: loginPayload,
|
|
48
|
+
status,
|
|
49
|
+
user: loginUser?.id,
|
|
50
|
+
provider: providerName,
|
|
51
|
+
error,
|
|
52
|
+
}, {
|
|
53
|
+
database: this.knex,
|
|
54
|
+
schema: this.schema,
|
|
55
|
+
accountability: this.accountability,
|
|
56
|
+
});
|
|
57
|
+
};
|
|
45
58
|
let userId;
|
|
46
59
|
try {
|
|
47
60
|
userId = await provider.getUserID(cloneDeep(payload));
|
|
48
61
|
}
|
|
49
62
|
catch (err) {
|
|
63
|
+
emitStatus('fail', payload, undefined, err);
|
|
50
64
|
await stall(STALL_TIME, timeStart);
|
|
51
65
|
throw err;
|
|
52
66
|
}
|
|
@@ -64,22 +78,11 @@ export class AuthenticationService {
|
|
|
64
78
|
schema: this.schema,
|
|
65
79
|
accountability: this.accountability,
|
|
66
80
|
});
|
|
67
|
-
const emitStatus = (status) => {
|
|
68
|
-
emitter.emitAction('auth.login', {
|
|
69
|
-
payload: updatedPayload,
|
|
70
|
-
status,
|
|
71
|
-
user: user?.id,
|
|
72
|
-
provider: providerName,
|
|
73
|
-
}, {
|
|
74
|
-
database: this.knex,
|
|
75
|
-
schema: this.schema,
|
|
76
|
-
accountability: this.accountability,
|
|
77
|
-
});
|
|
78
|
-
};
|
|
79
81
|
if (user?.status !== 'active' || user?.provider !== providerName) {
|
|
80
|
-
|
|
82
|
+
const loginError = new InvalidCredentialsError();
|
|
83
|
+
emitStatus('fail', updatedPayload, user, loginError);
|
|
81
84
|
await stall(STALL_TIME, timeStart);
|
|
82
|
-
throw
|
|
85
|
+
throw loginError;
|
|
83
86
|
}
|
|
84
87
|
const settingsService = new SettingsService({
|
|
85
88
|
knex: this.knex,
|
|
@@ -130,23 +133,25 @@ export class AuthenticationService {
|
|
|
130
133
|
try {
|
|
131
134
|
await provider.login(clone(user), cloneDeep(updatedPayload));
|
|
132
135
|
}
|
|
133
|
-
catch (
|
|
134
|
-
emitStatus('fail');
|
|
136
|
+
catch (err) {
|
|
137
|
+
emitStatus('fail', updatedPayload, user, err);
|
|
135
138
|
await stall(STALL_TIME, timeStart);
|
|
136
|
-
throw
|
|
139
|
+
throw err;
|
|
137
140
|
}
|
|
138
141
|
if (user.tfa_secret && !options?.otp) {
|
|
139
|
-
|
|
142
|
+
const loginError = new InvalidOtpError();
|
|
143
|
+
emitStatus('fail', updatedPayload, user, loginError);
|
|
140
144
|
await stall(STALL_TIME, timeStart);
|
|
141
|
-
throw
|
|
145
|
+
throw loginError;
|
|
142
146
|
}
|
|
143
147
|
if (user.tfa_secret && options?.otp) {
|
|
144
148
|
const tfaService = new TFAService({ knex: this.knex, schema: this.schema });
|
|
145
149
|
const otpValid = await tfaService.verifyOTP(user.id, options?.otp);
|
|
146
150
|
if (otpValid === false) {
|
|
147
|
-
|
|
151
|
+
const loginError = new InvalidOtpError();
|
|
152
|
+
emitStatus('fail', updatedPayload, user, loginError);
|
|
148
153
|
await stall(STALL_TIME, timeStart);
|
|
149
|
-
throw
|
|
154
|
+
throw loginError;
|
|
150
155
|
}
|
|
151
156
|
}
|
|
152
157
|
const roles = await fetchRolesTree(user.role, { knex: this.knex });
|
|
@@ -214,7 +219,7 @@ export class AuthenticationService {
|
|
|
214
219
|
});
|
|
215
220
|
}
|
|
216
221
|
await this.knex('directus_users').update({ last_access: new Date() }).where({ id: user.id });
|
|
217
|
-
emitStatus('success');
|
|
222
|
+
emitStatus('success', updatedPayload, user);
|
|
218
223
|
if (allowedAttempts !== null) {
|
|
219
224
|
await loginAttemptsLimiter.set(user.id, 0, 0);
|
|
220
225
|
}
|
|
@@ -52,6 +52,7 @@ export class CollectionsService {
|
|
|
52
52
|
if (payload.collection.startsWith('directus_')) {
|
|
53
53
|
throw new InvalidPayloadError({ reason: `Collections can't start with "directus_"` });
|
|
54
54
|
}
|
|
55
|
+
payload.collection = await this.helpers.schema.parseCollectionName(payload.collection);
|
|
55
56
|
const nestedActionEvents = [];
|
|
56
57
|
try {
|
|
57
58
|
const existingCollections = [
|
|
@@ -1,20 +1,49 @@
|
|
|
1
|
-
import type { AbstractServiceOptions, PrimaryKey } from '@directus/types';
|
|
1
|
+
import type { AbstractServiceOptions, PrimaryKey, Project as ProviderProject, ProviderType } from '@directus/types';
|
|
2
2
|
import { ItemsService } from './items.js';
|
|
3
3
|
export interface DeploymentProject {
|
|
4
4
|
id: string;
|
|
5
5
|
deployment: string;
|
|
6
6
|
external_id: string;
|
|
7
7
|
name: string;
|
|
8
|
+
url: string | null;
|
|
9
|
+
framework: string | null;
|
|
10
|
+
deployable: boolean;
|
|
8
11
|
date_created: string;
|
|
9
12
|
user_created: string;
|
|
10
13
|
}
|
|
11
14
|
export declare class DeploymentProjectsService extends ItemsService<DeploymentProject> {
|
|
12
15
|
constructor(options: AbstractServiceOptions);
|
|
16
|
+
/**
|
|
17
|
+
* Find a project by its provider-side external ID. Returns null if not tracked.
|
|
18
|
+
*/
|
|
19
|
+
readByExternalId(externalId: string): Promise<DeploymentProject | null>;
|
|
20
|
+
/**
|
|
21
|
+
* List provider projects merged with DB selection, syncing metadata.
|
|
22
|
+
*/
|
|
23
|
+
listWithSync(deploymentId: string, providerProjects: ProviderProject[]): Promise<{
|
|
24
|
+
id: string | null;
|
|
25
|
+
external_id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
deployable: boolean;
|
|
28
|
+
framework: string | undefined;
|
|
29
|
+
}[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Validate that all projects to create are deployable.
|
|
32
|
+
*/
|
|
33
|
+
validateDeployable(provider: ProviderType, projectsToCreate: {
|
|
34
|
+
external_id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
}[]): Promise<void>;
|
|
13
37
|
/**
|
|
14
38
|
* Update project selection (create/delete)
|
|
15
39
|
*/
|
|
16
|
-
updateSelection(
|
|
40
|
+
updateSelection(provider: ProviderType, create: {
|
|
17
41
|
external_id: string;
|
|
18
42
|
name: string;
|
|
19
43
|
}[], deleteIds: PrimaryKey[]): Promise<DeploymentProject[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Read deployment config by provider (null accountability for internal use).
|
|
46
|
+
*/
|
|
47
|
+
private readConfig;
|
|
48
|
+
private createDriver;
|
|
20
49
|
}
|
|
@@ -1,15 +1,92 @@
|
|
|
1
|
+
import { InvalidPayloadError } from '@directus/errors';
|
|
1
2
|
import getDatabase from '../database/index.js';
|
|
3
|
+
import { getDeploymentDriver } from '../deployment.js';
|
|
4
|
+
import { parseValue } from '../utils/parse-value.js';
|
|
2
5
|
import { transaction } from '../utils/transaction.js';
|
|
3
6
|
import { ItemsService } from './items.js';
|
|
4
7
|
export class DeploymentProjectsService extends ItemsService {
|
|
5
8
|
constructor(options) {
|
|
6
9
|
super('directus_deployment_projects', options);
|
|
7
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Find a project by its provider-side external ID. Returns null if not tracked.
|
|
13
|
+
*/
|
|
14
|
+
async readByExternalId(externalId) {
|
|
15
|
+
const results = await this.readByQuery({
|
|
16
|
+
filter: { external_id: { _eq: externalId } },
|
|
17
|
+
limit: 1,
|
|
18
|
+
});
|
|
19
|
+
return results?.[0] ?? null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* List provider projects merged with DB selection, syncing metadata.
|
|
23
|
+
*/
|
|
24
|
+
async listWithSync(deploymentId, providerProjects) {
|
|
25
|
+
const selectedProjects = await this.readByQuery({
|
|
26
|
+
filter: { deployment: { _eq: deploymentId } },
|
|
27
|
+
limit: -1,
|
|
28
|
+
});
|
|
29
|
+
const selectedMap = new Map(selectedProjects.map((p) => [p.external_id, p]));
|
|
30
|
+
// Sync name and deployable
|
|
31
|
+
const toUpdate = selectedProjects
|
|
32
|
+
.map((dbProject) => {
|
|
33
|
+
const providerProject = providerProjects.find((p) => p.id === dbProject.external_id);
|
|
34
|
+
if (!providerProject)
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
id: dbProject.id,
|
|
38
|
+
name: providerProject.name,
|
|
39
|
+
deployable: providerProject.deployable,
|
|
40
|
+
};
|
|
41
|
+
})
|
|
42
|
+
.filter((update) => update !== null);
|
|
43
|
+
if (toUpdate.length > 0) {
|
|
44
|
+
await this.updateBatch(toUpdate);
|
|
45
|
+
}
|
|
46
|
+
return providerProjects.map((project) => ({
|
|
47
|
+
id: selectedMap.get(project.id)?.id ?? null,
|
|
48
|
+
external_id: project.id,
|
|
49
|
+
name: project.name,
|
|
50
|
+
deployable: project.deployable,
|
|
51
|
+
framework: project.framework,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate that all projects to create are deployable.
|
|
56
|
+
*/
|
|
57
|
+
async validateDeployable(provider, projectsToCreate) {
|
|
58
|
+
if (projectsToCreate.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
const config = await this.readConfig(provider);
|
|
61
|
+
const driver = this.createDriver(config);
|
|
62
|
+
const providerProjects = await driver.listProjects();
|
|
63
|
+
const projectsMap = new Map(providerProjects.map((p) => [p.id, p]));
|
|
64
|
+
const nonDeployable = projectsToCreate.filter((p) => !projectsMap.get(p.external_id)?.deployable);
|
|
65
|
+
if (nonDeployable.length > 0) {
|
|
66
|
+
const names = nonDeployable.map((p) => projectsMap.get(p.external_id)?.name || p.external_id).join(', ');
|
|
67
|
+
throw new InvalidPayloadError({
|
|
68
|
+
reason: `Cannot add non-deployable projects: ${names}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
8
72
|
/**
|
|
9
73
|
* Update project selection (create/delete)
|
|
10
74
|
*/
|
|
11
|
-
async updateSelection(
|
|
75
|
+
async updateSelection(provider, create, deleteIds) {
|
|
76
|
+
const config = await this.readConfig(provider);
|
|
12
77
|
const db = getDatabase();
|
|
78
|
+
const driver = this.createDriver(config);
|
|
79
|
+
// Fetch metadata for new projects
|
|
80
|
+
const enrichedCreate = await Promise.all(create.map(async (p) => {
|
|
81
|
+
const details = await driver.getProject(p.external_id);
|
|
82
|
+
return {
|
|
83
|
+
external_id: p.external_id,
|
|
84
|
+
name: p.name,
|
|
85
|
+
url: details.url ?? null,
|
|
86
|
+
framework: details.framework ?? null,
|
|
87
|
+
deployable: details.deployable,
|
|
88
|
+
};
|
|
89
|
+
}));
|
|
13
90
|
return transaction(db, async (trx) => {
|
|
14
91
|
const trxService = new DeploymentProjectsService({
|
|
15
92
|
accountability: this.accountability,
|
|
@@ -19,16 +96,43 @@ export class DeploymentProjectsService extends ItemsService {
|
|
|
19
96
|
if (deleteIds.length > 0) {
|
|
20
97
|
await trxService.deleteMany(deleteIds);
|
|
21
98
|
}
|
|
22
|
-
if (
|
|
23
|
-
await trxService.createMany(
|
|
24
|
-
deployment:
|
|
99
|
+
if (enrichedCreate.length > 0) {
|
|
100
|
+
await trxService.createMany(enrichedCreate.map((p) => ({
|
|
101
|
+
deployment: config.id,
|
|
25
102
|
external_id: p.external_id,
|
|
26
103
|
name: p.name,
|
|
104
|
+
url: p.url,
|
|
105
|
+
framework: p.framework,
|
|
106
|
+
deployable: p.deployable,
|
|
27
107
|
})));
|
|
28
108
|
}
|
|
29
109
|
return trxService.readByQuery({
|
|
30
|
-
filter: { deployment: { _eq:
|
|
110
|
+
filter: { deployment: { _eq: config.id } },
|
|
111
|
+
limit: -1,
|
|
31
112
|
});
|
|
32
113
|
});
|
|
33
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Read deployment config by provider (null accountability for internal use).
|
|
117
|
+
*/
|
|
118
|
+
async readConfig(provider) {
|
|
119
|
+
const internalService = new ItemsService('directus_deployments', {
|
|
120
|
+
knex: this.knex,
|
|
121
|
+
schema: this.schema,
|
|
122
|
+
accountability: null,
|
|
123
|
+
});
|
|
124
|
+
const results = await internalService.readByQuery({
|
|
125
|
+
filter: { provider: { _eq: provider } },
|
|
126
|
+
limit: 1,
|
|
127
|
+
});
|
|
128
|
+
if (!results || results.length === 0) {
|
|
129
|
+
throw new Error(`Deployment config for "${provider}" not found`);
|
|
130
|
+
}
|
|
131
|
+
return results[0];
|
|
132
|
+
}
|
|
133
|
+
createDriver(config) {
|
|
134
|
+
const credentials = parseValue(config.credentials, {});
|
|
135
|
+
const options = parseValue(config.options, {});
|
|
136
|
+
return getDeploymentDriver(config.provider, credentials, options);
|
|
137
|
+
}
|
|
34
138
|
}
|
|
@@ -1,13 +1,31 @@
|
|
|
1
|
-
import type { AbstractServiceOptions } from '@directus/types';
|
|
1
|
+
import type { AbstractServiceOptions, DeploymentWebhookEvent } from '@directus/types';
|
|
2
2
|
import { ItemsService } from './items.js';
|
|
3
3
|
export interface DeploymentRun {
|
|
4
4
|
id: string;
|
|
5
5
|
project: string;
|
|
6
6
|
external_id: string;
|
|
7
7
|
target: string;
|
|
8
|
+
status: string | null;
|
|
9
|
+
url: string | null;
|
|
10
|
+
started_at: string | null;
|
|
11
|
+
completed_at: string | null;
|
|
8
12
|
date_created: string;
|
|
9
13
|
user_created: string;
|
|
10
14
|
}
|
|
11
15
|
export declare class DeploymentRunsService extends ItemsService<DeploymentRun> {
|
|
12
16
|
constructor(options: AbstractServiceOptions);
|
|
17
|
+
/**
|
|
18
|
+
* Process a webhook event: create or update a run based on the event data.
|
|
19
|
+
* Returns the run ID.
|
|
20
|
+
*/
|
|
21
|
+
processWebhookEvent(projectId: string, event: DeploymentWebhookEvent): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Get run stats for a project within a date range
|
|
24
|
+
*/
|
|
25
|
+
getStats(projectId: string, sinceDate: string): Promise<{
|
|
26
|
+
total_deployments: number;
|
|
27
|
+
average_build_time: number | null;
|
|
28
|
+
failed_builds: number;
|
|
29
|
+
successful_builds: number;
|
|
30
|
+
}>;
|
|
13
31
|
}
|
|
@@ -3,4 +3,90 @@ export class DeploymentRunsService extends ItemsService {
|
|
|
3
3
|
constructor(options) {
|
|
4
4
|
super('directus_deployment_runs', options);
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Process a webhook event: create or update a run based on the event data.
|
|
8
|
+
* Returns the run ID.
|
|
9
|
+
*/
|
|
10
|
+
async processWebhookEvent(projectId, event) {
|
|
11
|
+
const existingRuns = await this.readByQuery({
|
|
12
|
+
filter: {
|
|
13
|
+
project: { _eq: projectId },
|
|
14
|
+
external_id: { _eq: event.deployment_external_id },
|
|
15
|
+
},
|
|
16
|
+
limit: 1,
|
|
17
|
+
});
|
|
18
|
+
const isTerminal = event.type === 'deployment.succeeded' ||
|
|
19
|
+
event.type === 'deployment.error' ||
|
|
20
|
+
event.type === 'deployment.canceled';
|
|
21
|
+
if (existingRuns && existingRuns.length > 0) {
|
|
22
|
+
const existingRun = existingRuns[0];
|
|
23
|
+
await this.updateOne(existingRun.id, {
|
|
24
|
+
status: event.status,
|
|
25
|
+
...(event.url ? { url: event.url } : {}),
|
|
26
|
+
...(event.type === 'deployment.created' && !existingRun.started_at
|
|
27
|
+
? { started_at: event.timestamp.toISOString() }
|
|
28
|
+
: {}),
|
|
29
|
+
...(isTerminal ? { completed_at: event.timestamp.toISOString() } : {}),
|
|
30
|
+
});
|
|
31
|
+
return existingRun.id;
|
|
32
|
+
}
|
|
33
|
+
return (await this.createOne({
|
|
34
|
+
project: projectId,
|
|
35
|
+
external_id: event.deployment_external_id,
|
|
36
|
+
target: event.target || 'production',
|
|
37
|
+
status: event.status,
|
|
38
|
+
...(event.url ? { url: event.url } : {}),
|
|
39
|
+
started_at: event.type === 'deployment.created' ? event.timestamp.toISOString() : null,
|
|
40
|
+
...(isTerminal ? { completed_at: event.timestamp.toISOString() } : {}),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get run stats for a project within a date range
|
|
45
|
+
*/
|
|
46
|
+
async getStats(projectId, sinceDate) {
|
|
47
|
+
const dateFilter = {
|
|
48
|
+
_and: [{ project: { _eq: projectId } }, { date_created: { _gte: sinceDate } }],
|
|
49
|
+
};
|
|
50
|
+
const [countResult, completedRuns, statusCounts] = await Promise.all([
|
|
51
|
+
this.readByQuery({
|
|
52
|
+
filter: dateFilter,
|
|
53
|
+
aggregate: { count: ['*'] },
|
|
54
|
+
}),
|
|
55
|
+
this.readByQuery({
|
|
56
|
+
filter: {
|
|
57
|
+
_and: [
|
|
58
|
+
{ project: { _eq: projectId } },
|
|
59
|
+
{ date_created: { _gte: sinceDate } },
|
|
60
|
+
{ started_at: { _nnull: true } },
|
|
61
|
+
{ completed_at: { _nnull: true } },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
fields: ['started_at', 'completed_at'],
|
|
65
|
+
limit: -1,
|
|
66
|
+
}),
|
|
67
|
+
this.readByQuery({
|
|
68
|
+
filter: {
|
|
69
|
+
_and: [
|
|
70
|
+
{ project: { _eq: projectId } },
|
|
71
|
+
{ date_created: { _gte: sinceDate } },
|
|
72
|
+
{ status: { _in: ['ready', 'error'] } },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
aggregate: { count: ['*'] },
|
|
76
|
+
group: ['status'],
|
|
77
|
+
}),
|
|
78
|
+
]);
|
|
79
|
+
let averageBuildTime = null;
|
|
80
|
+
if (completedRuns.length > 0) {
|
|
81
|
+
const durations = completedRuns.map((r) => new Date(r.completed_at).getTime() - new Date(r.started_at).getTime());
|
|
82
|
+
averageBuildTime = Math.round(durations.reduce((a, b) => a + b, 0) / durations.length);
|
|
83
|
+
}
|
|
84
|
+
const statusMap = new Map(statusCounts.map((r) => [r.status, Number(r.count)]));
|
|
85
|
+
return {
|
|
86
|
+
total_deployments: Number(countResult[0]?.['count'] ?? 0),
|
|
87
|
+
average_build_time: averageBuildTime,
|
|
88
|
+
failed_builds: statusMap.get('error') ?? 0,
|
|
89
|
+
successful_builds: statusMap.get('ready') ?? 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
6
92
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { AbstractServiceOptions, CachedResult, DeploymentConfig, PrimaryKey, Project, ProviderType, Query } from '@directus/types';
|
|
1
|
+
import type { AbstractServiceOptions, CachedResult, Credentials, DeploymentConfig, Options, PrimaryKey, Project, ProviderType, Query } from '@directus/types';
|
|
2
2
|
import type { DeploymentDriver } from '../deployment/deployment.js';
|
|
3
|
+
import type { DeploymentRun } from './deployment-runs.js';
|
|
3
4
|
import { ItemsService } from './items.js';
|
|
4
5
|
export declare class DeploymentService extends ItemsService<DeploymentConfig> {
|
|
5
6
|
constructor(options: AbstractServiceOptions);
|
|
@@ -22,13 +23,21 @@ export declare class DeploymentService extends ItemsService<DeploymentConfig> {
|
|
|
22
23
|
*/
|
|
23
24
|
private readConfig;
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
+
* Get webhook config for a provider
|
|
26
27
|
*/
|
|
27
|
-
|
|
28
|
+
getWebhookConfig(provider: ProviderType): Promise<{
|
|
29
|
+
webhook_secret: string | null;
|
|
30
|
+
credentials: Credentials;
|
|
31
|
+
options: Options;
|
|
32
|
+
}>;
|
|
28
33
|
/**
|
|
29
34
|
* Get a deployment driver instance with decrypted credentials
|
|
30
35
|
*/
|
|
31
36
|
getDriver(provider: ProviderType): Promise<DeploymentDriver>;
|
|
37
|
+
/**
|
|
38
|
+
* Sync webhook registration with current tracked projects.
|
|
39
|
+
*/
|
|
40
|
+
syncWebhook(provider: ProviderType): Promise<void>;
|
|
32
41
|
/**
|
|
33
42
|
* List projects from provider with caching
|
|
34
43
|
*/
|
|
@@ -37,4 +46,36 @@ export declare class DeploymentService extends ItemsService<DeploymentConfig> {
|
|
|
37
46
|
* Get project details from provider with caching
|
|
38
47
|
*/
|
|
39
48
|
getProviderProject(provider: ProviderType, projectId: string): Promise<CachedResult<Project>>;
|
|
49
|
+
/**
|
|
50
|
+
* Dashboard: projects + latest run status + stats
|
|
51
|
+
*/
|
|
52
|
+
getDashboard(provider: ProviderType, sinceDate: Date): Promise<{
|
|
53
|
+
projects: any[];
|
|
54
|
+
stats: {
|
|
55
|
+
active_deployments: number;
|
|
56
|
+
successful_builds: number;
|
|
57
|
+
failed_builds: number;
|
|
58
|
+
};
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Refresh project metadata (name, url, framework, deployable) if stale.
|
|
62
|
+
*/
|
|
63
|
+
private syncProjectMetadataIfStale;
|
|
64
|
+
/**
|
|
65
|
+
* Trigger a deployment for a project
|
|
66
|
+
*/
|
|
67
|
+
triggerDeployment(provider: ProviderType, projectId: string, options: {
|
|
68
|
+
preview: boolean;
|
|
69
|
+
clearCache: boolean;
|
|
70
|
+
}): Promise<DeploymentRun>;
|
|
71
|
+
/**
|
|
72
|
+
* Cancel a deployment run
|
|
73
|
+
*/
|
|
74
|
+
cancelDeployment(provider: ProviderType, runId: string): Promise<DeploymentRun>;
|
|
75
|
+
/**
|
|
76
|
+
* Get a run with its logs from the provider
|
|
77
|
+
*/
|
|
78
|
+
getRunWithLogs(provider: ProviderType, runId: string, since?: Date): Promise<DeploymentRun & {
|
|
79
|
+
logs: any;
|
|
80
|
+
}>;
|
|
40
81
|
}
|