@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
|
@@ -2,7 +2,7 @@ import { useEnv } from '@directus/env';
|
|
|
2
2
|
import { ErrorCode, InvalidCredentialsError, InvalidPayloadError, InvalidProviderConfigError, InvalidProviderError, isDirectusError, ServiceUnavailableError, UnexpectedResponseError, } from '@directus/errors';
|
|
3
3
|
import { Router } from 'express';
|
|
4
4
|
import Joi from 'joi';
|
|
5
|
-
import
|
|
5
|
+
import { Client, InappropriateAuthError, InsufficientAccessError, InvalidCredentialsError as LdapInvalidCredentialsError, } from 'ldapts';
|
|
6
6
|
import { REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from '../../constants.js';
|
|
7
7
|
import getDatabase from '../../database/index.js';
|
|
8
8
|
import emitter from '../../emitter.js';
|
|
@@ -34,127 +34,101 @@ export class LDAPAuthDriver extends AuthDriver {
|
|
|
34
34
|
throw new InvalidProviderConfigError({ provider });
|
|
35
35
|
}
|
|
36
36
|
const clientConfig = typeof config['client'] === 'object' ? config['client'] : {};
|
|
37
|
-
this.bindClient =
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
this.bindClient = new Client({
|
|
38
|
+
url: clientUrl,
|
|
39
|
+
...clientConfig,
|
|
40
40
|
});
|
|
41
41
|
this.config = config;
|
|
42
42
|
}
|
|
43
43
|
async validateBindClient() {
|
|
44
44
|
const logger = useLogger();
|
|
45
45
|
const { bindDn, bindPassword, provider } = this.config;
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
this.bindClient.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
res.on('searchEntry', () => {
|
|
54
|
-
resolve();
|
|
55
|
-
});
|
|
56
|
-
res.on('error', () => {
|
|
57
|
-
// Attempt to rebind on search error
|
|
58
|
-
this.bindClient.bind(bindDn, bindPassword, (err) => {
|
|
59
|
-
if (err) {
|
|
60
|
-
const error = handleError(err);
|
|
61
|
-
if (isDirectusError(error, ErrorCode.InvalidCredentials)) {
|
|
62
|
-
logger.warn('Invalid bind user');
|
|
63
|
-
reject(new InvalidProviderConfigError({ provider }));
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
reject(error);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
resolve();
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
res.on('end', (result) => {
|
|
75
|
-
// Handle edge case where authenticated bind user cannot read their own DN
|
|
76
|
-
// Status `0` is success
|
|
77
|
-
if (result?.status !== 0) {
|
|
78
|
-
logger.warn('[LDAP] Failed to find bind user record');
|
|
79
|
-
reject(new UnexpectedResponseError());
|
|
80
|
-
}
|
|
81
|
-
});
|
|
46
|
+
try {
|
|
47
|
+
// Attempt to bind with the configured credentials
|
|
48
|
+
await this.bindClient.bind(bindDn, bindPassword);
|
|
49
|
+
// Healthcheck: verify bind user can read their own DN
|
|
50
|
+
const { searchEntries } = await this.bindClient.search(bindDn, {
|
|
51
|
+
scope: 'base',
|
|
82
52
|
});
|
|
83
|
-
|
|
53
|
+
if (searchEntries.length === 0) {
|
|
54
|
+
logger.warn('[LDAP] Failed to find bind user record');
|
|
55
|
+
throw new UnexpectedResponseError();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const error = handleError(err);
|
|
60
|
+
if (isDirectusError(error, ErrorCode.InvalidCredentials)) {
|
|
61
|
+
logger.warn('Invalid bind user');
|
|
62
|
+
throw new InvalidProviderConfigError({ provider });
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
84
66
|
}
|
|
85
67
|
async fetchUserInfo(baseDn, filter, scope) {
|
|
86
68
|
let { firstNameAttribute, lastNameAttribute, mailAttribute } = this.config;
|
|
87
69
|
firstNameAttribute ??= 'givenName';
|
|
88
70
|
lastNameAttribute ??= 'sn';
|
|
89
71
|
mailAttribute ??= 'mail';
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.bindClient.search(baseDn, {
|
|
93
|
-
filter,
|
|
94
|
-
scope,
|
|
72
|
+
try {
|
|
73
|
+
const searchOptions = {
|
|
95
74
|
attributes: ['uid', firstNameAttribute, lastNameAttribute, mailAttribute, 'userAccountControl'],
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
});
|
|
75
|
+
};
|
|
76
|
+
if (filter !== undefined)
|
|
77
|
+
searchOptions.filter = filter;
|
|
78
|
+
if (scope !== undefined)
|
|
79
|
+
searchOptions.scope = scope;
|
|
80
|
+
const { searchEntries } = await this.bindClient.search(baseDn, searchOptions);
|
|
81
|
+
if (searchEntries.length === 0) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const entry = searchEntries[0];
|
|
85
|
+
const user = {
|
|
86
|
+
dn: entry['dn'],
|
|
87
|
+
userAccountControl: Number(getEntryValue(entry['userAccountControl']) ?? 0),
|
|
88
|
+
};
|
|
89
|
+
const firstName = getEntryValue(entry[firstNameAttribute]);
|
|
90
|
+
if (firstName)
|
|
91
|
+
user.firstName = firstName;
|
|
92
|
+
const lastName = getEntryValue(entry[lastNameAttribute]);
|
|
93
|
+
if (lastName)
|
|
94
|
+
user.lastName = lastName;
|
|
95
|
+
const email = getEntryValue(entry[mailAttribute]);
|
|
96
|
+
if (email)
|
|
97
|
+
user.email = email;
|
|
98
|
+
const uid = getEntryValue(entry['uid']);
|
|
99
|
+
if (uid)
|
|
100
|
+
user.uid = uid;
|
|
101
|
+
return user;
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
throw handleError(err);
|
|
105
|
+
}
|
|
128
106
|
}
|
|
129
107
|
async fetchUserGroups(baseDn, filter, scope) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// Search for the user info in LDAP by group attribute
|
|
133
|
-
this.bindClient.search(baseDn, {
|
|
134
|
-
filter,
|
|
135
|
-
scope,
|
|
108
|
+
try {
|
|
109
|
+
const searchOptions = {
|
|
136
110
|
attributes: ['cn'],
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
111
|
+
};
|
|
112
|
+
if (filter !== undefined)
|
|
113
|
+
searchOptions.filter = filter;
|
|
114
|
+
if (scope !== undefined)
|
|
115
|
+
searchOptions.scope = scope;
|
|
116
|
+
const { searchEntries } = await this.bindClient.search(baseDn, searchOptions);
|
|
117
|
+
const userGroups = [];
|
|
118
|
+
for (const entry of searchEntries) {
|
|
119
|
+
const cn = entry['cn'];
|
|
120
|
+
if (Array.isArray(cn)) {
|
|
121
|
+
userGroups.push(...cn.map((v) => String(v)));
|
|
141
122
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
reject(handleError(err));
|
|
152
|
-
});
|
|
153
|
-
res.on('end', () => {
|
|
154
|
-
resolve(userGroups);
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
});
|
|
123
|
+
else if (cn) {
|
|
124
|
+
userGroups.push(String(cn));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return userGroups;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
throw handleError(err);
|
|
131
|
+
}
|
|
158
132
|
}
|
|
159
133
|
async fetchUserId(userDn) {
|
|
160
134
|
const user = await this.knex
|
|
@@ -171,19 +145,15 @@ export class LDAPAuthDriver extends AuthDriver {
|
|
|
171
145
|
const logger = useLogger();
|
|
172
146
|
await this.validateBindClient();
|
|
173
147
|
const { userDn, userScope, userAttribute, groupDn, groupScope, groupAttribute, defaultRoleId, syncUserInfo } = this.config;
|
|
174
|
-
const userInfo = await this.fetchUserInfo(userDn,
|
|
175
|
-
attribute: userAttribute ?? 'cn',
|
|
176
|
-
value: payload['identifier'],
|
|
177
|
-
}), userScope ?? 'one');
|
|
148
|
+
const userInfo = await this.fetchUserInfo(userDn, `(${validateLDAPAttribute(userAttribute ?? 'cn')}=${escapeFilterValue(payload['identifier'])})`, userScope ?? 'one');
|
|
178
149
|
if (!userInfo?.dn) {
|
|
179
150
|
throw new InvalidCredentialsError();
|
|
180
151
|
}
|
|
181
152
|
let userRole;
|
|
182
153
|
if (groupDn) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}), groupScope ?? 'one');
|
|
154
|
+
const groupAttr = groupAttribute ?? 'member';
|
|
155
|
+
const memberValue = groupAttr.toLowerCase() === 'memberuid' && userInfo.uid ? userInfo.uid : userInfo.dn;
|
|
156
|
+
const userGroups = await this.fetchUserGroups(groupDn, `(${validateLDAPAttribute(groupAttr)}=${escapeFilterValue(memberValue)})`, groupScope ?? 'one');
|
|
187
157
|
if (userGroups.length) {
|
|
188
158
|
userRole = await this.knex
|
|
189
159
|
.select('id')
|
|
@@ -253,51 +223,86 @@ export class LDAPAuthDriver extends AuthDriver {
|
|
|
253
223
|
if (!user.external_identifier || !password) {
|
|
254
224
|
throw new InvalidCredentialsError();
|
|
255
225
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
...clientConfig,
|
|
261
|
-
reconnect: false,
|
|
262
|
-
});
|
|
263
|
-
client.on('error', (err) => {
|
|
264
|
-
reject(handleError(err));
|
|
265
|
-
});
|
|
266
|
-
client.bind(user.external_identifier, password, (err) => {
|
|
267
|
-
if (err) {
|
|
268
|
-
reject(handleError(err));
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
resolve();
|
|
272
|
-
}
|
|
273
|
-
client.destroy();
|
|
274
|
-
});
|
|
226
|
+
const clientConfig = typeof this.config['client'] === 'object' ? this.config['client'] : {};
|
|
227
|
+
const client = new Client({
|
|
228
|
+
url: this.config['clientUrl'],
|
|
229
|
+
...clientConfig,
|
|
275
230
|
});
|
|
231
|
+
try {
|
|
232
|
+
await client.bind(user.external_identifier, password);
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
throw handleError(err);
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
await client.unbind().catch(() => {
|
|
239
|
+
// Ignore unbind errors
|
|
240
|
+
});
|
|
241
|
+
}
|
|
276
242
|
}
|
|
277
243
|
async login(user, payload) {
|
|
278
244
|
await this.verify(user, payload['password']);
|
|
279
245
|
}
|
|
280
246
|
async refresh(user) {
|
|
281
247
|
await this.validateBindClient();
|
|
282
|
-
|
|
248
|
+
// Use scope 'base' to search the specific DN entry
|
|
249
|
+
const userInfo = await this.fetchUserInfo(user.external_identifier, undefined, 'base');
|
|
283
250
|
if (userInfo?.userAccountControl && userInfo.userAccountControl & INVALID_ACCOUNT_FLAGS) {
|
|
284
251
|
throw new InvalidCredentialsError();
|
|
285
252
|
}
|
|
286
253
|
}
|
|
287
254
|
}
|
|
288
255
|
const handleError = (e) => {
|
|
289
|
-
if (e instanceof
|
|
290
|
-
e instanceof
|
|
291
|
-
e instanceof
|
|
256
|
+
if (e instanceof InappropriateAuthError ||
|
|
257
|
+
e instanceof LdapInvalidCredentialsError ||
|
|
258
|
+
e instanceof InsufficientAccessError) {
|
|
292
259
|
return new InvalidCredentialsError();
|
|
293
260
|
}
|
|
261
|
+
if (e instanceof Error) {
|
|
262
|
+
return new ServiceUnavailableError({
|
|
263
|
+
service: 'ldap',
|
|
264
|
+
reason: `Service returned unexpected error: ${e.message}`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
294
267
|
return new ServiceUnavailableError({
|
|
295
268
|
service: 'ldap',
|
|
296
|
-
reason:
|
|
269
|
+
reason: 'Service returned unexpected error',
|
|
297
270
|
});
|
|
298
271
|
};
|
|
299
272
|
const getEntryValue = (value) => {
|
|
300
|
-
|
|
273
|
+
if (value === undefined)
|
|
274
|
+
return undefined;
|
|
275
|
+
if (Buffer.isBuffer(value)) {
|
|
276
|
+
return value.toString();
|
|
277
|
+
}
|
|
278
|
+
if (Array.isArray(value)) {
|
|
279
|
+
const first = value[0];
|
|
280
|
+
if (Buffer.isBuffer(first)) {
|
|
281
|
+
return first.toString();
|
|
282
|
+
}
|
|
283
|
+
return first;
|
|
284
|
+
}
|
|
285
|
+
return value;
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Escape special characters in LDAP filter values according to RFC 4515
|
|
289
|
+
*/
|
|
290
|
+
const escapeFilterValue = (value) => {
|
|
291
|
+
return value
|
|
292
|
+
.replace(/\\/g, '\\5c')
|
|
293
|
+
.replace(/\*/g, '\\2a')
|
|
294
|
+
.replace(/\(/g, '\\28')
|
|
295
|
+
.replace(/\)/g, '\\29')
|
|
296
|
+
.replace(/\0/g, '\\00');
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* Validate LDAP attribute name according to RFC 4512
|
|
300
|
+
*/
|
|
301
|
+
const validateLDAPAttribute = (name) => {
|
|
302
|
+
if (/^[a-zA-Z][a-zA-Z0-9-]*$/.test(name) === false) {
|
|
303
|
+
throw new Error(`Invalid LDAP attribute name: "${name}"`);
|
|
304
|
+
}
|
|
305
|
+
return name;
|
|
301
306
|
};
|
|
302
307
|
export function createLDAPAuthRouter(provider) {
|
|
303
308
|
const router = Router();
|
package/dist/cache.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import Keyv from 'keyv';
|
|
|
3
3
|
export declare function getCache(): {
|
|
4
4
|
cache: Keyv | null;
|
|
5
5
|
systemCache: Keyv;
|
|
6
|
+
deploymentCache: Keyv;
|
|
6
7
|
localSchemaCache: Keyv;
|
|
7
8
|
lockCache: Keyv;
|
|
8
9
|
};
|
|
@@ -17,3 +18,14 @@ export declare function setMemorySchemaCache(schema: SchemaOverview): void;
|
|
|
17
18
|
export declare function getMemorySchemaCache(): Readonly<SchemaOverview> | undefined;
|
|
18
19
|
export declare function setCacheValue(cache: Keyv, key: string, value: Record<string, any> | Record<string, any>[], ttl?: number): Promise<void>;
|
|
19
20
|
export declare function getCacheValue(cache: Keyv, key: string): Promise<any>;
|
|
21
|
+
/**
|
|
22
|
+
* Store a value in cache with its expiration timestamp for TTL tracking
|
|
23
|
+
*/
|
|
24
|
+
export declare function setCacheValueWithExpiry(cache: Keyv, key: string, value: Record<string, any> | Record<string, any>[], ttl: number): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Get a cached value along with its remaining TTL
|
|
27
|
+
*/
|
|
28
|
+
export declare function getCacheValueWithTTL(cache: Keyv, key: string): Promise<{
|
|
29
|
+
data: any;
|
|
30
|
+
remainingTTL: number;
|
|
31
|
+
} | undefined>;
|
package/dist/cache.js
CHANGED
|
@@ -15,6 +15,7 @@ const env = useEnv();
|
|
|
15
15
|
const require = createRequire(import.meta.url);
|
|
16
16
|
let cache = null;
|
|
17
17
|
let systemCache = null;
|
|
18
|
+
let deploymentCache = null;
|
|
18
19
|
let lockCache = null;
|
|
19
20
|
let messengerSubscribed = false;
|
|
20
21
|
let localSchemaCache = null;
|
|
@@ -40,6 +41,11 @@ export function getCache() {
|
|
|
40
41
|
systemCache = getKeyvInstance(env['CACHE_STORE'], getMilliseconds(env['CACHE_SYSTEM_TTL']), '_system');
|
|
41
42
|
systemCache.on('error', (err) => logger.warn(err, `[system-cache] ${err}`));
|
|
42
43
|
}
|
|
44
|
+
if (deploymentCache === null) {
|
|
45
|
+
const ttl = getMilliseconds(env['CACHE_DEPLOYMENT_TTL']) || 5000; // Default 5s
|
|
46
|
+
deploymentCache = getKeyvInstance(env['CACHE_STORE'], ttl, '_deployment');
|
|
47
|
+
deploymentCache.on('error', (err) => logger.warn(err, `[deployment-cache] ${err}`));
|
|
48
|
+
}
|
|
43
49
|
if (localSchemaCache === null) {
|
|
44
50
|
localSchemaCache = getKeyvInstance('memory', getMilliseconds(env['CACHE_SYSTEM_TTL']), '_schema');
|
|
45
51
|
localSchemaCache.on('error', (err) => logger.warn(err, `[schema-cache] ${err}`));
|
|
@@ -48,7 +54,7 @@ export function getCache() {
|
|
|
48
54
|
lockCache = getKeyvInstance(env['CACHE_STORE'], undefined, '_lock');
|
|
49
55
|
lockCache.on('error', (err) => logger.warn(err, `[lock-cache] ${err}`));
|
|
50
56
|
}
|
|
51
|
-
return { cache, systemCache, localSchemaCache, lockCache };
|
|
57
|
+
return { cache, systemCache, deploymentCache, localSchemaCache, lockCache };
|
|
52
58
|
}
|
|
53
59
|
export async function flushCaches(forced) {
|
|
54
60
|
const { cache } = getCache();
|
|
@@ -107,6 +113,24 @@ export async function getCacheValue(cache, key) {
|
|
|
107
113
|
const decompressed = await decompress(value);
|
|
108
114
|
return decompressed;
|
|
109
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Store a value in cache with its expiration timestamp for TTL tracking
|
|
118
|
+
*/
|
|
119
|
+
export async function setCacheValueWithExpiry(cache, key, value, ttl) {
|
|
120
|
+
await setCacheValue(cache, key, value, ttl);
|
|
121
|
+
await setCacheValue(cache, `${key}__expires_at`, { exp: Date.now() + ttl }, ttl);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get a cached value along with its remaining TTL
|
|
125
|
+
*/
|
|
126
|
+
export async function getCacheValueWithTTL(cache, key) {
|
|
127
|
+
const value = await getCacheValue(cache, key);
|
|
128
|
+
if (!value)
|
|
129
|
+
return undefined;
|
|
130
|
+
const expiryData = await getCacheValue(cache, `${key}__expires_at`);
|
|
131
|
+
const remainingTTL = expiryData?.exp ? Math.max(0, expiryData.exp - Date.now()) : 0;
|
|
132
|
+
return { data: value, remainingTTL };
|
|
133
|
+
}
|
|
110
134
|
function getKeyvInstance(store, ttl, namespaceSuffix) {
|
|
111
135
|
switch (store) {
|
|
112
136
|
case 'redis':
|
|
@@ -148,6 +148,9 @@ CACHE_ENABLED=false
|
|
|
148
148
|
# How long the cache is persisted ["5m"]
|
|
149
149
|
# CACHE_TTL="30m"
|
|
150
150
|
|
|
151
|
+
# How long deployment provider data is cached ["5s"]
|
|
152
|
+
# CACHE_DEPLOYMENT_TTL="5s"
|
|
153
|
+
|
|
151
154
|
# How to scope the cache data ["system-cache"]
|
|
152
155
|
# CACHE_NAMESPACE="system-cache"
|
|
153
156
|
|