@directus/api 27.0.2 → 28.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/auth/drivers/oauth2.js +4 -4
- package/dist/auth/drivers/openid.d.ts +2 -1
- package/dist/auth/drivers/openid.js +78 -43
- package/dist/database/errors/dialects/mssql.d.ts +2 -1
- package/dist/database/errors/dialects/mssql.js +124 -120
- package/dist/database/errors/dialects/mysql.d.ts +2 -1
- package/dist/database/errors/dialects/mysql.js +112 -108
- package/dist/database/errors/dialects/postgres.d.ts +2 -1
- package/dist/database/errors/dialects/postgres.js +75 -71
- package/dist/database/errors/dialects/sqlite.d.ts +2 -1
- package/dist/database/errors/dialects/sqlite.js +6 -5
- package/dist/database/errors/translate.d.ts +2 -1
- package/dist/database/errors/translate.js +5 -5
- package/dist/database/get-ast-from-query/lib/convert-wildcards.d.ts +5 -3
- package/dist/database/get-ast-from-query/lib/convert-wildcards.js +26 -16
- package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +2 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +5 -4
- package/dist/database/migrations/20250609A-license-banner.d.ts +3 -0
- package/dist/database/migrations/20250609A-license-banner.js +14 -0
- package/dist/database/migrations/20250613A-add-project-id.d.ts +3 -0
- package/dist/database/migrations/20250613A-add-project-id.js +26 -0
- package/dist/extensions/lib/get-extensions-settings.js +14 -8
- package/dist/extensions/manager.js +26 -0
- package/dist/flows.d.ts +5 -1
- package/dist/flows.js +61 -4
- package/dist/operations/condition/index.js +1 -1
- package/dist/permissions/utils/get-permissions-for-share.js +2 -0
- package/dist/permissions/utils/merge-fields.d.ts +1 -0
- package/dist/permissions/utils/merge-fields.js +29 -0
- package/dist/permissions/utils/merge-permissions.js +3 -14
- package/dist/services/fields.js +3 -3
- package/dist/services/graphql/resolvers/mutation.js +1 -1
- package/dist/services/graphql/resolvers/query.js +1 -1
- package/dist/services/graphql/resolvers/system.js +4 -4
- package/dist/services/graphql/schema/parse-query.d.ts +1 -1
- package/dist/services/graphql/schema/parse-query.js +8 -1
- package/dist/services/graphql/schema/read.js +4 -4
- package/dist/services/graphql/subscription.js +1 -1
- package/dist/services/graphql/utils/filter-replace-m2a.d.ts +3 -0
- package/dist/services/graphql/utils/filter-replace-m2a.js +59 -0
- package/dist/services/items.js +2 -2
- package/dist/services/payload.js +2 -2
- package/dist/services/relations.d.ts +1 -1
- package/dist/services/relations.js +5 -2
- package/dist/services/specifications.js +6 -2
- package/dist/services/users.js +3 -0
- package/dist/telemetry/lib/get-report.js +4 -1
- package/dist/telemetry/types/report.d.ts +4 -0
- package/dist/telemetry/utils/get-project-id.d.ts +2 -0
- package/dist/telemetry/utils/get-project-id.js +4 -0
- package/dist/utils/get-ip-from-req.d.ts +2 -1
- package/dist/utils/get-ip-from-req.js +29 -2
- package/dist/utils/get-schema.js +1 -1
- package/dist/utils/is-url-allowed.js +1 -1
- package/dist/utils/sanitize-query.js +6 -0
- package/dist/utils/validate-query.js +1 -0
- package/dist/websocket/controllers/base.d.ts +2 -2
- package/dist/websocket/controllers/base.js +33 -5
- package/dist/websocket/types.d.ts +1 -0
- package/dist/websocket/utils/items.d.ts +1 -1
- package/package.json +27 -24
- package/dist/utils/map-values-deep.d.ts +0 -1
- package/dist/utils/map-values-deep.js +0 -25
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { ErrorCode, InvalidCredentialsError, InvalidPayloadError, InvalidProviderConfigError, InvalidProviderError, InvalidTokenError, isDirectusError, ServiceUnavailableError, } from '@directus/errors';
|
|
3
|
-
import { parseJSON } from '@directus/utils';
|
|
3
|
+
import { parseJSON, toArray } from '@directus/utils';
|
|
4
4
|
import express, { Router } from 'express';
|
|
5
5
|
import { flatten } from 'flat';
|
|
6
6
|
import jwt from 'jsonwebtoken';
|
|
@@ -126,8 +126,8 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
126
126
|
}
|
|
127
127
|
let role = this.config['defaultRoleId'];
|
|
128
128
|
const groupClaimName = this.config['groupClaimName'] ?? 'groups';
|
|
129
|
-
const groups = userInfo[groupClaimName];
|
|
130
|
-
if (
|
|
129
|
+
const groups = userInfo[groupClaimName] ? toArray(userInfo[groupClaimName]) : [];
|
|
130
|
+
if (groups.length > 0) {
|
|
131
131
|
for (const key in this.roleMap) {
|
|
132
132
|
if (groups.includes(key)) {
|
|
133
133
|
// Overwrite default role if user is member of a group specified in roleMap
|
|
@@ -136,7 +136,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
-
else {
|
|
139
|
+
else if (Object.keys(this.roleMap).length > 0) {
|
|
140
140
|
logger.debug(`[OAuth2] Configured group claim with name "${groupClaimName}" does not exist or is empty.`);
|
|
141
141
|
}
|
|
142
142
|
// Flatten response to support dot indexes
|
|
@@ -5,12 +5,13 @@ import type { AuthDriverOptions, User } from '../../types/index.js';
|
|
|
5
5
|
import type { RoleMap } from '../../types/rolemap.js';
|
|
6
6
|
import { LocalAuthDriver } from './local.js';
|
|
7
7
|
export declare class OpenIDAuthDriver extends LocalAuthDriver {
|
|
8
|
-
client:
|
|
8
|
+
client: null | Client;
|
|
9
9
|
redirectUrl: string;
|
|
10
10
|
usersService: UsersService;
|
|
11
11
|
config: Record<string, any>;
|
|
12
12
|
roleMap: RoleMap;
|
|
13
13
|
constructor(options: AuthDriverOptions, config: Record<string, any>);
|
|
14
|
+
private getClient;
|
|
14
15
|
generateCodeVerifier(): string;
|
|
15
16
|
generateAuthUrl(codeVerifier: string, prompt?: boolean): Promise<string>;
|
|
16
17
|
private fetchUserId;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { ErrorCode, InvalidCredentialsError, InvalidPayloadError, InvalidProviderConfigError, InvalidProviderError, InvalidTokenError, isDirectusError, ServiceUnavailableError, } from '@directus/errors';
|
|
3
|
-
import { parseJSON } from '@directus/utils';
|
|
3
|
+
import { parseJSON, toArray } from '@directus/utils';
|
|
4
4
|
import express, { Router } from 'express';
|
|
5
5
|
import { flatten } from 'flat';
|
|
6
6
|
import jwt from 'jsonwebtoken';
|
|
7
|
-
import { errors, generators, Issuer } from 'openid-client';
|
|
7
|
+
import { custom, errors, generators, Issuer } from 'openid-client';
|
|
8
8
|
import { getAuthProvider } from '../../auth.js';
|
|
9
9
|
import { REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from '../../constants.js';
|
|
10
10
|
import getDatabase from '../../database/index.js';
|
|
@@ -32,23 +32,15 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
32
32
|
super(options, config);
|
|
33
33
|
const env = useEnv();
|
|
34
34
|
const logger = useLogger();
|
|
35
|
-
const { issuerUrl, clientId, clientSecret,
|
|
36
|
-
if (!issuerUrl || !clientId || !clientSecret || !
|
|
35
|
+
const { issuerUrl, clientId, clientSecret, provider, issuerDiscoveryMustSucceed } = config;
|
|
36
|
+
if (!issuerUrl || !clientId || !clientSecret || !provider) {
|
|
37
37
|
logger.error('Invalid provider config');
|
|
38
|
-
throw new InvalidProviderConfigError({ provider
|
|
38
|
+
throw new InvalidProviderConfigError({ provider });
|
|
39
39
|
}
|
|
40
|
-
const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login',
|
|
41
|
-
// extract client overrides/options excluding CLIENT_ID and CLIENT_SECRET as they are passed directly
|
|
42
|
-
const clientOptionsOverrides = getConfigFromEnv(`AUTH_${config['provider'].toUpperCase()}_CLIENT_`, {
|
|
43
|
-
omitKey: [
|
|
44
|
-
`AUTH_${config['provider'].toUpperCase()}_CLIENT_ID`,
|
|
45
|
-
`AUTH_${config['provider'].toUpperCase()}_CLIENT_SECRET`,
|
|
46
|
-
],
|
|
47
|
-
type: 'underscore',
|
|
48
|
-
});
|
|
40
|
+
const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login', provider, 'callback');
|
|
49
41
|
this.redirectUrl = redirectUrl.toString();
|
|
50
42
|
this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
|
|
51
|
-
this.config =
|
|
43
|
+
this.config = config;
|
|
52
44
|
this.roleMap = {};
|
|
53
45
|
const roleMapping = this.config['roleMapping'];
|
|
54
46
|
if (roleMapping) {
|
|
@@ -60,29 +52,62 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
60
52
|
logger.error("[OpenID] Expected a JSON-Object as role mapping, got an Array instead. Make sure you declare the variable with 'json:' prefix.");
|
|
61
53
|
throw new InvalidProviderError();
|
|
62
54
|
}
|
|
63
|
-
this.client =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
reject(new InvalidProviderConfigError({
|
|
70
|
-
provider: additionalConfig['provider'],
|
|
71
|
-
}));
|
|
72
|
-
}
|
|
73
|
-
resolve(new issuer.Client({
|
|
74
|
-
client_id: clientId,
|
|
75
|
-
client_secret: clientSecret,
|
|
76
|
-
redirect_uris: [this.redirectUrl],
|
|
77
|
-
response_types: ['code'],
|
|
78
|
-
...clientOptionsOverrides,
|
|
79
|
-
}));
|
|
80
|
-
})
|
|
81
|
-
.catch((e) => {
|
|
82
|
-
logger.error(e, '[OpenID] Failed to fetch provider config');
|
|
55
|
+
this.client = null;
|
|
56
|
+
// preload client
|
|
57
|
+
this.getClient().catch((e) => {
|
|
58
|
+
logger.error(e, '[OpenID] Failed to fetch provider config');
|
|
59
|
+
if (issuerDiscoveryMustSucceed !== false) {
|
|
60
|
+
logger.error(`AUTH_${provider.toUpperCase()}_ISSUER_DISCOVERY_MUST_SUCCEED is enabled and discovery failed, exiting`);
|
|
83
61
|
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async getClient() {
|
|
66
|
+
if (this.client)
|
|
67
|
+
return this.client;
|
|
68
|
+
const logger = useLogger();
|
|
69
|
+
const { issuerUrl, clientId, clientSecret, provider } = this.config;
|
|
70
|
+
// extract client http overrides/options
|
|
71
|
+
const clientHttpOptions = getConfigFromEnv(`AUTH_${provider.toUpperCase()}_CLIENT_HTTP_`);
|
|
72
|
+
if (clientHttpOptions) {
|
|
73
|
+
Issuer[custom.http_options] = (_, options) => {
|
|
74
|
+
return {
|
|
75
|
+
...options,
|
|
76
|
+
...clientHttpOptions,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const issuer = await Issuer.discover(issuerUrl);
|
|
81
|
+
const supportedTypes = issuer.metadata['response_types_supported'];
|
|
82
|
+
if (!supportedTypes?.includes('code')) {
|
|
83
|
+
logger.error('OpenID provider does not support required code flow');
|
|
84
|
+
throw new InvalidProviderConfigError({
|
|
85
|
+
provider,
|
|
84
86
|
});
|
|
87
|
+
}
|
|
88
|
+
// extract client overrides/options excluding CLIENT_ID and CLIENT_SECRET as they are passed directly
|
|
89
|
+
const clientOptionsOverrides = getConfigFromEnv(`AUTH_${provider.toUpperCase()}_CLIENT_`, {
|
|
90
|
+
omitKey: [`AUTH_${provider.toUpperCase()}_CLIENT_ID`, `AUTH_${provider.toUpperCase()}_CLIENT_SECRET`],
|
|
91
|
+
omitPrefix: [`AUTH_${provider.toUpperCase()}_CLIENT_HTTP_`],
|
|
92
|
+
type: 'underscore',
|
|
93
|
+
});
|
|
94
|
+
const client = new issuer.Client({
|
|
95
|
+
client_id: clientId,
|
|
96
|
+
client_secret: clientSecret,
|
|
97
|
+
redirect_uris: [this.redirectUrl],
|
|
98
|
+
response_types: ['code'],
|
|
99
|
+
...clientOptionsOverrides,
|
|
85
100
|
});
|
|
101
|
+
if (clientHttpOptions) {
|
|
102
|
+
client[custom.http_options] = (_, options) => {
|
|
103
|
+
return {
|
|
104
|
+
...options,
|
|
105
|
+
...clientHttpOptions,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
this.client = client;
|
|
110
|
+
return client;
|
|
86
111
|
}
|
|
87
112
|
generateCodeVerifier() {
|
|
88
113
|
return generators.codeVerifier();
|
|
@@ -90,7 +115,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
90
115
|
async generateAuthUrl(codeVerifier, prompt = false) {
|
|
91
116
|
const { plainCodeChallenge } = this.config;
|
|
92
117
|
try {
|
|
93
|
-
const client = await this.
|
|
118
|
+
const client = await this.getClient();
|
|
94
119
|
const codeChallenge = plainCodeChallenge ? codeVerifier : generators.codeChallenge(codeVerifier);
|
|
95
120
|
const paramsConfig = typeof this.config['params'] === 'object' ? this.config['params'] : {};
|
|
96
121
|
return client.authorizationUrl({
|
|
@@ -127,7 +152,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
127
152
|
let tokenSet;
|
|
128
153
|
let userInfo;
|
|
129
154
|
try {
|
|
130
|
-
const client = await this.
|
|
155
|
+
const client = await this.getClient();
|
|
131
156
|
const codeChallenge = plainCodeChallenge
|
|
132
157
|
? payload['codeVerifier']
|
|
133
158
|
: generators.codeChallenge(payload['codeVerifier']);
|
|
@@ -145,8 +170,8 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
145
170
|
}
|
|
146
171
|
let role = this.config['defaultRoleId'];
|
|
147
172
|
const groupClaimName = this.config['groupClaimName'] ?? 'groups';
|
|
148
|
-
const groups = userInfo[groupClaimName];
|
|
149
|
-
if (
|
|
173
|
+
const groups = userInfo[groupClaimName] ? toArray(userInfo[groupClaimName]) : [];
|
|
174
|
+
if (groups.length > 0) {
|
|
150
175
|
for (const key in this.roleMap) {
|
|
151
176
|
if (groups.includes(key)) {
|
|
152
177
|
// Overwrite default role if user is member of a group specified in roleMap
|
|
@@ -155,7 +180,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
155
180
|
}
|
|
156
181
|
}
|
|
157
182
|
}
|
|
158
|
-
else {
|
|
183
|
+
else if (Object.keys(this.roleMap).length > 0) {
|
|
159
184
|
logger.debug(`[OpenID] Configured group claim with name "${groupClaimName}" does not exist or is empty.`);
|
|
160
185
|
}
|
|
161
186
|
// Flatten response to support dot indexes
|
|
@@ -248,7 +273,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
248
273
|
}
|
|
249
274
|
if (authData?.['refreshToken']) {
|
|
250
275
|
try {
|
|
251
|
-
const client = await this.
|
|
276
|
+
const client = await this.getClient();
|
|
252
277
|
const tokenSet = await client.refresh(authData['refreshToken']);
|
|
253
278
|
// Update user refreshToken if provided
|
|
254
279
|
if (tokenSet.refresh_token) {
|
|
@@ -305,12 +330,21 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
305
330
|
httpOnly: true,
|
|
306
331
|
sameSite: 'lax',
|
|
307
332
|
});
|
|
308
|
-
|
|
333
|
+
try {
|
|
334
|
+
return res.redirect(await provider.generateAuthUrl(codeVerifier, prompt));
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return res.redirect(new Url(env['PUBLIC_URL'])
|
|
338
|
+
.addPath('admin', 'login')
|
|
339
|
+
.setQuery('reason', ErrorCode.ServiceUnavailable)
|
|
340
|
+
.toString());
|
|
341
|
+
}
|
|
309
342
|
}), respond);
|
|
310
343
|
router.post('/callback', express.urlencoded({ extended: false }), (req, res) => {
|
|
311
344
|
res.redirect(303, `./callback?${new URLSearchParams(req.body)}`);
|
|
312
345
|
}, respond);
|
|
313
346
|
router.get('/callback', asyncHandler(async (req, res, next) => {
|
|
347
|
+
const env = useEnv();
|
|
314
348
|
const logger = useLogger();
|
|
315
349
|
let tokenData;
|
|
316
350
|
try {
|
|
@@ -318,7 +352,8 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
318
352
|
}
|
|
319
353
|
catch (e) {
|
|
320
354
|
logger.warn(e, `[OpenID] Couldn't verify OpenID cookie`);
|
|
321
|
-
|
|
355
|
+
const url = new Url(env['PUBLIC_URL']).addPath('admin', 'login');
|
|
356
|
+
return res.redirect(`${url.toString()}?reason=${ErrorCode.InvalidCredentials}`);
|
|
322
357
|
}
|
|
323
358
|
const { verifier, redirect, prompt } = tokenData;
|
|
324
359
|
const accountability = createDefaultAccountability({ ip: getIPFromReq(req) });
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { MSSQLError } from './types.js';
|
|
2
|
-
|
|
2
|
+
import type { Item } from '@directus/types';
|
|
3
|
+
export declare function extractError(error: MSSQLError, data: Partial<Item>): Promise<MSSQLError | Error>;
|
|
@@ -8,135 +8,139 @@ var MSSQLErrorCodes;
|
|
|
8
8
|
MSSQLErrorCodes[MSSQLErrorCodes["UNIQUE_VIOLATION"] = 2601] = "UNIQUE_VIOLATION";
|
|
9
9
|
MSSQLErrorCodes[MSSQLErrorCodes["VALUE_LIMIT_VIOLATION"] = 2628] = "VALUE_LIMIT_VIOLATION";
|
|
10
10
|
})(MSSQLErrorCodes || (MSSQLErrorCodes = {}));
|
|
11
|
-
export async function extractError(error) {
|
|
11
|
+
export async function extractError(error, data) {
|
|
12
12
|
switch (error.number) {
|
|
13
13
|
case MSSQLErrorCodes.UNIQUE_VIOLATION:
|
|
14
14
|
case 2627:
|
|
15
|
-
return await uniqueViolation(
|
|
15
|
+
return await uniqueViolation();
|
|
16
16
|
case MSSQLErrorCodes.NUMERIC_VALUE_OUT_OF_RANGE:
|
|
17
|
-
return numericValueOutOfRange(
|
|
17
|
+
return numericValueOutOfRange();
|
|
18
18
|
case MSSQLErrorCodes.VALUE_LIMIT_VIOLATION:
|
|
19
|
-
return valueLimitViolation(
|
|
19
|
+
return valueLimitViolation();
|
|
20
20
|
case MSSQLErrorCodes.NOT_NULL_VIOLATION:
|
|
21
|
-
return notNullViolation(
|
|
21
|
+
return notNullViolation();
|
|
22
22
|
case MSSQLErrorCodes.FOREIGN_KEY_VIOLATION:
|
|
23
|
-
return foreignKeyViolation(
|
|
23
|
+
return foreignKeyViolation();
|
|
24
24
|
}
|
|
25
25
|
return error;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
26
|
+
async function uniqueViolation() {
|
|
27
|
+
/**
|
|
28
|
+
* NOTE:
|
|
29
|
+
* SQL Server doesn't return the name of the offending column when a unique constraint is thrown:
|
|
30
|
+
*
|
|
31
|
+
* insert into [articles] ([unique]) values (@p0)
|
|
32
|
+
* - Violation of UNIQUE KEY constraint 'UQ__articles__5A062640242004EB'.
|
|
33
|
+
* Cannot insert duplicate key in object 'dbo.articles'. The duplicate key value is (rijk).
|
|
34
|
+
*
|
|
35
|
+
* While it's not ideal, the best next thing we can do is extract the column name from
|
|
36
|
+
* information_schema when this happens
|
|
37
|
+
*/
|
|
38
|
+
const betweenQuotes = /'([^']+)'/g;
|
|
39
|
+
const betweenParens = /\(([^)]+)\)/g;
|
|
40
|
+
const quoteMatches = error.message.match(betweenQuotes);
|
|
41
|
+
const parenMatches = error.message.match(betweenParens);
|
|
42
|
+
if (!quoteMatches || !parenMatches)
|
|
43
|
+
return error;
|
|
44
|
+
const keyName = quoteMatches[1].slice(1, -1);
|
|
45
|
+
let collection = quoteMatches[0].slice(1, -1);
|
|
46
|
+
let field = null;
|
|
47
|
+
if (keyName) {
|
|
48
|
+
const database = getDatabase();
|
|
49
|
+
const constraintUsage = await database
|
|
50
|
+
.select('sys.columns.name as field', database.raw('OBJECT_NAME(??) as collection', ['sys.columns.object_id']))
|
|
51
|
+
.from('sys.indexes')
|
|
52
|
+
.innerJoin('sys.index_columns', (join) => {
|
|
53
|
+
join
|
|
54
|
+
.on('sys.indexes.object_id', '=', 'sys.index_columns.object_id')
|
|
55
|
+
.andOn('sys.indexes.index_id', '=', 'sys.index_columns.index_id');
|
|
56
|
+
})
|
|
57
|
+
.innerJoin('sys.columns', (join) => {
|
|
58
|
+
join
|
|
59
|
+
.on('sys.index_columns.object_id', '=', 'sys.columns.object_id')
|
|
60
|
+
.andOn('sys.index_columns.column_id', '=', 'sys.columns.column_id');
|
|
61
|
+
})
|
|
62
|
+
.where('sys.indexes.name', '=', keyName)
|
|
63
|
+
.first();
|
|
64
|
+
collection = constraintUsage?.collection;
|
|
65
|
+
field = constraintUsage?.field;
|
|
66
|
+
}
|
|
67
|
+
return new RecordNotUniqueError({
|
|
68
|
+
collection,
|
|
69
|
+
field,
|
|
70
|
+
value: field ? data[field] : null,
|
|
71
|
+
});
|
|
67
72
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
73
|
+
function numericValueOutOfRange() {
|
|
74
|
+
const betweenBrackets = /\[([^\]]+)\]/g;
|
|
75
|
+
const bracketMatches = error.message.match(betweenBrackets);
|
|
76
|
+
if (!bracketMatches)
|
|
77
|
+
return error;
|
|
78
|
+
const collection = bracketMatches[0].slice(1, -1);
|
|
79
|
+
/**
|
|
80
|
+
* NOTE
|
|
81
|
+
* MS SQL Doesn't return the offending column name in the error, nor any other identifying information
|
|
82
|
+
* we can use to extract the column name :(
|
|
83
|
+
*
|
|
84
|
+
* insert into [test1] ([small]) values (@p0)
|
|
85
|
+
* - Arithmetic overflow error for data type tinyint, value = 50000.
|
|
86
|
+
*/
|
|
87
|
+
const field = null;
|
|
88
|
+
return new ValueOutOfRangeError({
|
|
89
|
+
collection,
|
|
90
|
+
field,
|
|
91
|
+
value: field ? data[field] : null,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function valueLimitViolation() {
|
|
95
|
+
const betweenBrackets = /\[([^\]]+)\]/g;
|
|
96
|
+
const betweenQuotes = /'([^']+)'/g;
|
|
97
|
+
const bracketMatches = error.message.match(betweenBrackets);
|
|
98
|
+
const quoteMatches = error.message.match(betweenQuotes);
|
|
99
|
+
if (!bracketMatches || !quoteMatches)
|
|
100
|
+
return error;
|
|
101
|
+
const collection = bracketMatches[0].slice(1, -1);
|
|
102
|
+
const field = quoteMatches[1].slice(1, -1);
|
|
103
|
+
return new ValueTooLongError({
|
|
104
|
+
collection,
|
|
105
|
+
field,
|
|
106
|
+
value: field ? data[field] : null,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function notNullViolation() {
|
|
110
|
+
const betweenBrackets = /\[([^\]]+)\]/g;
|
|
111
|
+
const betweenQuotes = /'([^']+)'/g;
|
|
112
|
+
const bracketMatches = error.message.match(betweenBrackets);
|
|
113
|
+
const quoteMatches = error.message.match(betweenQuotes);
|
|
114
|
+
if (!bracketMatches || !quoteMatches)
|
|
115
|
+
return error;
|
|
116
|
+
const collection = bracketMatches[0].slice(1, -1);
|
|
117
|
+
const field = quoteMatches[0].slice(1, -1);
|
|
118
|
+
if (error.message.includes('Cannot insert the value NULL into column')) {
|
|
119
|
+
return new ContainsNullValuesError({ collection, field });
|
|
120
|
+
}
|
|
121
|
+
return new NotNullViolationError({
|
|
122
|
+
collection,
|
|
123
|
+
field,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function foreignKeyViolation() {
|
|
127
|
+
const betweenUnderscores = /__(.+)__/g;
|
|
128
|
+
const betweenParens = /\(([^)]+)\)/g;
|
|
129
|
+
// NOTE:
|
|
130
|
+
// Seeing that MS SQL doesn't return the offending column name, we have to extract it from the
|
|
131
|
+
// foreign key constraint name as generated by the database. This'll probably fail if you have
|
|
132
|
+
// custom names for whatever reason.
|
|
133
|
+
const underscoreMatches = error.message.match(betweenUnderscores);
|
|
134
|
+
const parenMatches = error.message.match(betweenParens);
|
|
135
|
+
if (!underscoreMatches || !parenMatches)
|
|
136
|
+
return error;
|
|
137
|
+
const underscoreParts = underscoreMatches[0].split('__');
|
|
138
|
+
const collection = underscoreParts[1];
|
|
139
|
+
const field = underscoreParts[2];
|
|
140
|
+
return new InvalidForeignKeyError({
|
|
141
|
+
collection,
|
|
142
|
+
field,
|
|
143
|
+
value: field ? data[field] : null,
|
|
144
|
+
});
|
|
118
145
|
}
|
|
119
|
-
return new NotNullViolationError({
|
|
120
|
-
collection,
|
|
121
|
-
field,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
function foreignKeyViolation(error) {
|
|
125
|
-
const betweenUnderscores = /__(.+)__/g;
|
|
126
|
-
const betweenParens = /\(([^)]+)\)/g;
|
|
127
|
-
// NOTE:
|
|
128
|
-
// Seeing that MS SQL doesn't return the offending column name, we have to extract it from the
|
|
129
|
-
// foreign key constraint name as generated by the database. This'll probably fail if you have
|
|
130
|
-
// custom names for whatever reason.
|
|
131
|
-
const underscoreMatches = error.message.match(betweenUnderscores);
|
|
132
|
-
const parenMatches = error.message.match(betweenParens);
|
|
133
|
-
if (!underscoreMatches || !parenMatches)
|
|
134
|
-
return error;
|
|
135
|
-
const underscoreParts = underscoreMatches[0].split('__');
|
|
136
|
-
const collection = underscoreParts[1];
|
|
137
|
-
const field = underscoreParts[2];
|
|
138
|
-
return new InvalidForeignKeyError({
|
|
139
|
-
collection,
|
|
140
|
-
field,
|
|
141
|
-
});
|
|
142
146
|
}
|