mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
@@ -14,6 +14,7 @@ vi.mock('./database', () => ({
|
||||
|
||||
vi.mock('./env', async () => {
|
||||
const actual = (await vi.importActual('./env')) as { default: Record<string, any> };
|
||||
|
||||
const MOCK_ENV = {
|
||||
...actual.default,
|
||||
KEY: 'xxxxxxx-xxxxxx-xxxxxxxx-xxxxxxxxxx',
|
||||
@@ -170,9 +171,11 @@ describe('createApp', async () => {
|
||||
const testRoute = '/custom-endpoint-to-test';
|
||||
const testResponse = { key: 'value' };
|
||||
const mockRouter = Router();
|
||||
|
||||
mockRouter.use(testRoute, (_, res) => {
|
||||
res.json(testResponse);
|
||||
});
|
||||
|
||||
mockGetEndpointRouter.mockReturnValueOnce(mockRouter);
|
||||
|
||||
const app = await createApp();
|
||||
|
||||
@@ -194,6 +194,7 @@ export default async function createApp(): Promise<express.Application> {
|
||||
|
||||
// Set the App's base path according to the APIs public URL
|
||||
const html = await readFile(adminPath, 'utf8');
|
||||
|
||||
const htmlWithVars = html
|
||||
.replace(/<base \/>/, `<base href="${adminUrl.toString({ rootRelative: true })}/" />`)
|
||||
.replace(/<embed-head \/>/, embeds.head)
|
||||
|
||||
@@ -64,9 +64,11 @@ export class LDAPAuthDriver extends AuthDriver {
|
||||
const clientConfig = typeof config['client'] === 'object' ? config['client'] : {};
|
||||
|
||||
this.bindClient = ldap.createClient({ url: clientUrl, reconnect: true, ...clientConfig });
|
||||
|
||||
this.bindClient.on('error', (err: Error) => {
|
||||
logger.warn(err);
|
||||
});
|
||||
|
||||
this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
|
||||
this.config = config;
|
||||
}
|
||||
@@ -332,6 +334,7 @@ export class LDAPAuthDriver extends AuthDriver {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const clientConfig = typeof this.config['client'] === 'object' ? this.config['client'] : {};
|
||||
|
||||
const client = ldap.createClient({
|
||||
url: this.config['clientUrl'],
|
||||
...clientConfig,
|
||||
|
||||
@@ -121,6 +121,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
||||
{ code: payload['code'], state: payload['state'] },
|
||||
{ code_verifier: payload['codeVerifier'], state: generators.codeChallenge(payload['codeVerifier']) }
|
||||
);
|
||||
|
||||
userInfo = await this.client.userinfo(tokenSet.access_token!);
|
||||
} catch (e) {
|
||||
throw handleError(e);
|
||||
@@ -270,6 +271,7 @@ export function createOAuth2AuthRouter(providerName: string): Router {
|
||||
const provider = getAuthProvider(providerName) as OAuth2AuthDriver;
|
||||
const codeVerifier = provider.generateCodeVerifier();
|
||||
const prompt = !!req.query['prompt'];
|
||||
|
||||
const token = jwt.sign(
|
||||
{ verifier: codeVerifier, redirect: req.query['redirect'], prompt },
|
||||
env['SECRET'] as string,
|
||||
@@ -297,6 +299,7 @@ export function createOAuth2AuthRouter(providerName: string): Router {
|
||||
},
|
||||
respond
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/callback',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
@@ -337,6 +340,7 @@ export function createOAuth2AuthRouter(providerName: string): Router {
|
||||
|
||||
try {
|
||||
res.clearCookie(`oauth2.${providerName}`);
|
||||
|
||||
authResponse = await authenticationService.login(providerName, {
|
||||
code: req.query['code'],
|
||||
codeVerifier: verifier,
|
||||
|
||||
@@ -55,6 +55,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
||||
this.redirectUrl = redirectUrl.toString();
|
||||
this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
|
||||
this.config = additionalConfig;
|
||||
|
||||
this.client = new Promise((resolve, reject) => {
|
||||
Issuer.discover(issuerUrl)
|
||||
.then((issuer) => {
|
||||
@@ -133,11 +134,13 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
||||
try {
|
||||
const client = await this.client;
|
||||
const codeChallenge = generators.codeChallenge(payload['codeVerifier']);
|
||||
|
||||
tokenSet = await client.callback(
|
||||
this.redirectUrl,
|
||||
{ code: payload['code'], state: payload['state'], iss: payload['iss'] },
|
||||
{ code_verifier: payload['codeVerifier'], state: codeChallenge, nonce: codeChallenge }
|
||||
);
|
||||
|
||||
userInfo = tokenSet.claims();
|
||||
|
||||
if (client.issuer.metadata['userinfo_endpoint']) {
|
||||
@@ -297,6 +300,7 @@ export function createOpenIDAuthRouter(providerName: string): Router {
|
||||
const provider = getAuthProvider(providerName) as OpenIDAuthDriver;
|
||||
const codeVerifier = provider.generateCodeVerifier();
|
||||
const prompt = !!req.query['prompt'];
|
||||
|
||||
const token = jwt.sign(
|
||||
{ verifier: codeVerifier, redirect: req.query['redirect'], prompt },
|
||||
env['SECRET'] as string,
|
||||
@@ -365,6 +369,7 @@ export function createOpenIDAuthRouter(providerName: string): Router {
|
||||
|
||||
try {
|
||||
res.clearCookie(`openid.${providerName}`);
|
||||
|
||||
authResponse = await authenticationService.login(providerName, {
|
||||
code: req.query['code'],
|
||||
codeVerifier: verifier,
|
||||
|
||||
@@ -137,6 +137,7 @@ export async function apply(snapshotPath: string, options?: { yes: boolean; dryR
|
||||
}
|
||||
|
||||
message = 'The following changes will be applied:\n\n' + chalk.black(message);
|
||||
|
||||
if (dryRun) {
|
||||
logger.info(message);
|
||||
process.exit(0);
|
||||
|
||||
@@ -38,14 +38,17 @@ export async function createCli(): Promise<Command> {
|
||||
|
||||
const dbCommand = program.command('database');
|
||||
dbCommand.command('install').description('Install the database').action(dbInstall);
|
||||
|
||||
dbCommand
|
||||
.command('migrate:latest')
|
||||
.description('Upgrade the database')
|
||||
.action(() => dbMigrate('latest'));
|
||||
|
||||
dbCommand
|
||||
.command('migrate:up')
|
||||
.description('Upgrade the database')
|
||||
.action(() => dbMigrate('up'));
|
||||
|
||||
dbCommand
|
||||
.command('migrate:down')
|
||||
.description('Downgrade the database')
|
||||
@@ -69,6 +72,7 @@ export async function createCli(): Promise<Command> {
|
||||
.action(usersPasswd);
|
||||
|
||||
const rolesCommand = program.command('roles');
|
||||
|
||||
rolesCommand
|
||||
.command('create')
|
||||
.description('Create a new role')
|
||||
|
||||
@@ -29,6 +29,7 @@ router.get(
|
||||
const defaults = { storage_asset_presets: [], storage_asset_transform: 'all' };
|
||||
|
||||
const database = getDatabase();
|
||||
|
||||
const savedAssetSettings = await database
|
||||
.select('storage_asset_presets', 'storage_asset_transform')
|
||||
.from('directus_settings')
|
||||
@@ -81,6 +82,7 @@ router.get(
|
||||
}
|
||||
|
||||
const systemKeys = SYSTEM_ASSET_ALLOW_LIST.map((transformation) => transformation['key']!);
|
||||
|
||||
const allKeys: string[] = [
|
||||
...systemKeys,
|
||||
...(assetSettings.storage_asset_presets || []).map(
|
||||
|
||||
@@ -218,6 +218,7 @@ router.get(
|
||||
data: getAuthProviders(),
|
||||
disableDefault: env['AUTH_DISABLE_DEFAULT'],
|
||||
};
|
||||
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
|
||||
@@ -57,6 +57,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
|
||||
@@ -53,10 +53,12 @@ router.get(
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/javascript; charset=UTF-8');
|
||||
|
||||
res.setHeader(
|
||||
'Cache-Control',
|
||||
getCacheControlHeader(req, getMilliseconds(env['EXTENSIONS_CACHE_TTL']), false, false)
|
||||
);
|
||||
|
||||
res.setHeader('Vary', 'Origin, Cache-Control');
|
||||
res.end(source);
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ router.get(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const fields = await service.readAll();
|
||||
|
||||
res.locals['payload'] = { data: fields || null };
|
||||
@@ -37,6 +38,7 @@ router.get(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const fields = await service.readAll(req.params['collection']);
|
||||
|
||||
res.locals['payload'] = { data: fields || null };
|
||||
@@ -215,6 +217,7 @@ router.delete(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
await service.deleteField(req.params['collection']!, req.params['field']!);
|
||||
return next();
|
||||
}),
|
||||
|
||||
@@ -28,6 +28,7 @@ describe('multipartHandler', () => {
|
||||
params: {},
|
||||
pipe: (input: NodeJS.WritableStream) => stream.pipe(input),
|
||||
} as unknown as Request;
|
||||
|
||||
const res = {} as Response;
|
||||
|
||||
const stream = new PassThrough();
|
||||
@@ -56,6 +57,7 @@ describe('multipartHandler', () => {
|
||||
params: {},
|
||||
pipe: (input: NodeJS.WritableStream) => stream.pipe(input),
|
||||
} as unknown as Request;
|
||||
|
||||
const res = {} as Response;
|
||||
|
||||
const stream = new PassThrough();
|
||||
|
||||
@@ -128,6 +128,7 @@ router.post(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
let keys: PrimaryKey | PrimaryKey[] = [];
|
||||
|
||||
if (req.is('multipart/form-data')) {
|
||||
|
||||
@@ -84,6 +84,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
|
||||
@@ -57,6 +57,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
@@ -88,6 +89,7 @@ router.get(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const record = await service.readOne(req.params['pk']!, req.sanitizedQuery);
|
||||
|
||||
res.locals['payload'] = { data: record || null };
|
||||
|
||||
@@ -57,6 +57,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
|
||||
@@ -57,6 +57,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
|
||||
@@ -57,6 +57,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
|
||||
@@ -86,6 +86,7 @@ router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (req.path.endsWith('me')) return next();
|
||||
|
||||
const service = new PermissionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
|
||||
@@ -57,6 +57,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
|
||||
@@ -34,6 +34,7 @@ router.get(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const relations = await service.readAll(req.params['collection']);
|
||||
|
||||
res.locals['payload'] = { data: relations || null };
|
||||
@@ -156,6 +157,7 @@ router.delete(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
await service.deleteOne(req.params['collection']!, req.params['field']!);
|
||||
return next();
|
||||
}),
|
||||
|
||||
@@ -15,6 +15,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
|
||||
@@ -55,6 +55,7 @@ router.get(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const data = await service.serverInfo();
|
||||
res.locals['payload'] = { data };
|
||||
return next();
|
||||
|
||||
@@ -16,6 +16,7 @@ router.get(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const records = await service.readSingleton(req.sanitizedQuery);
|
||||
res.locals['payload'] = { data: records || null };
|
||||
return next();
|
||||
@@ -30,6 +31,7 @@ router.patch(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
await service.upsertSingleton(req.body);
|
||||
|
||||
try {
|
||||
|
||||
@@ -90,6 +90,7 @@ router.get(
|
||||
app_access: false,
|
||||
},
|
||||
};
|
||||
|
||||
res.locals['payload'] = { data: user };
|
||||
return next();
|
||||
}
|
||||
@@ -124,6 +125,7 @@ router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (req.path.endsWith('me')) return next();
|
||||
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
@@ -293,6 +295,7 @@ router.post(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
await service.inviteUser(req.body.email, req.body.role, req.body.invite_url || null);
|
||||
return next();
|
||||
}),
|
||||
@@ -309,10 +312,12 @@ router.post(
|
||||
asyncHandler(async (req, _res, next) => {
|
||||
const { error } = acceptInviteSchema.validate(req.body);
|
||||
if (error) throw new InvalidPayloadException(error.message);
|
||||
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
await service.acceptInvite(req.body.token, req.body.password);
|
||||
return next();
|
||||
}),
|
||||
@@ -339,6 +344,7 @@ router.post(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
await authService.verifyPassword(req.accountability.user, req.body.password);
|
||||
|
||||
const { url, secret } = await service.generateTFA(req.accountability.user);
|
||||
@@ -369,6 +375,7 @@ router.post(
|
||||
const rolesService = new RolesService({
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const role = (await rolesService.readOne(req.accountability.role)) as Role;
|
||||
|
||||
if (role && role.enforce_tfa) {
|
||||
@@ -423,6 +430,7 @@ router.post(
|
||||
const rolesService = new RolesService({
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const role = (await rolesService.readOne(req.accountability.role)) as Role;
|
||||
|
||||
if (role && role.enforce_tfa) {
|
||||
|
||||
@@ -80,6 +80,7 @@ router.post(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
await service.sort(req.collection, req.body);
|
||||
|
||||
return res.status(200).end();
|
||||
@@ -93,6 +94,7 @@ router.post(
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
await service.revert(req.params['revision']!);
|
||||
next();
|
||||
}),
|
||||
|
||||
@@ -57,6 +57,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
|
||||
@@ -132,6 +132,7 @@ export abstract class SchemaHelper extends DatabaseHelper {
|
||||
knex.ref('directus_row_number').toQuery(),
|
||||
knex.raw(`partition by ?? order by ${orderByString}`, [`${table}.${primaryKey}`, ...orderByFields])
|
||||
);
|
||||
|
||||
return dbQuery;
|
||||
}
|
||||
|
||||
|
||||
@@ -264,6 +264,7 @@ export async function validateMigrations(): Promise<boolean> {
|
||||
migrationFiles.push(...customMigrationFiles);
|
||||
|
||||
const requiredVersions = migrationFiles.map((filePath) => filePath.split('-')[0]);
|
||||
|
||||
const completedVersions = (await database.select('version').from('directus_migrations')).map(
|
||||
({ version }) => version
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
const inspector = createInspector(knex);
|
||||
|
||||
const foreignKeys = await inspector.foreignKeys();
|
||||
|
||||
const relations = await knex
|
||||
.select<RelationMeta[]>('id', 'many_collection', 'many_field', 'one_collection')
|
||||
.from('directus_relations');
|
||||
@@ -16,6 +17,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
const exists = !!foreignKeys.find(
|
||||
(fk) => fk.table === relation?.many_collection && fk.column === relation?.many_field
|
||||
);
|
||||
|
||||
return exists === false;
|
||||
});
|
||||
|
||||
@@ -43,6 +45,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
logger.warn(
|
||||
`Illegal relationship ${constraint.many_collection}.${constraint.many_field}<->${constraint.one_collection} encountered. Many field equals collections primary key.`
|
||||
);
|
||||
|
||||
corruptedRelations.push(constraint.id);
|
||||
continue;
|
||||
}
|
||||
@@ -101,6 +104,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
}
|
||||
|
||||
const indexName = getDefaultIndexName('foreign', constraint.many_collection, constraint.many_field);
|
||||
|
||||
const builder = table
|
||||
.foreign(constraint.many_field, indexName)
|
||||
.references(relatedPrimaryKeyField)
|
||||
@@ -115,6 +119,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
logger.warn(
|
||||
`Couldn't add foreign key constraint for ${constraint.many_collection}.${constraint.many_field}<->${constraint.one_collection}`
|
||||
);
|
||||
|
||||
logger.warn(err);
|
||||
}
|
||||
}
|
||||
@@ -144,6 +149,7 @@ export async function down(knex: Knex): Promise<void> {
|
||||
logger.warn(
|
||||
`Couldn't drop foreign key constraint for ${relation.many_collection}.${relation.many_field}<->${relation.one_collection}`
|
||||
);
|
||||
|
||||
logger.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
logger.warn(
|
||||
`Couldn't clean up index for foreign key ${update.table}.${constraint.column}->${constraint.references}`
|
||||
);
|
||||
|
||||
logger.warn(err);
|
||||
}
|
||||
}
|
||||
@@ -160,6 +161,7 @@ export async function down(knex: Knex): Promise<void> {
|
||||
logger.warn(
|
||||
`Couldn't clean up index for foreign key ${update.table}.${constraint.column}->${constraint.references}`
|
||||
);
|
||||
|
||||
logger.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getHelpers } from '../helpers/index.js';
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const helper = getHelpers(knex).schema;
|
||||
const type = helper.isOneOfClients(['oracle', 'cockroachdb']) ? 'text' : 'string';
|
||||
|
||||
await helper.changeToType('directus_webhooks', 'collections', type, {
|
||||
nullable: false,
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ describe('run', () => {
|
||||
expect(e.message).toBe('Nothing to upgrade');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns "Method implemented in the dialect driver" if no directus_migrations', async () => {
|
||||
tracker.on.select('directus_migrations').response([]);
|
||||
|
||||
@@ -34,6 +35,7 @@ describe('run', () => {
|
||||
expect(e.message).toBe('Method implemented in the dialect driver');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined if the migration is successful', async () => {
|
||||
tracker.on.select('directus_migrations').response([
|
||||
{
|
||||
@@ -42,12 +44,14 @@ describe('run', () => {
|
||||
timestamp: '2021-11-27 11:36:56.471595-05',
|
||||
},
|
||||
]);
|
||||
|
||||
tracker.on.delete('directus_relations').response([]);
|
||||
tracker.on.insert('directus_migrations').response(['Remove System Relations', '20201029A']);
|
||||
|
||||
expect(await run(db, 'up')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when passed the argument down', () => {
|
||||
it('returns "Nothing To downgrade" if no valid directus_migrations', async () => {
|
||||
tracker.on.select('directus_migrations').response(['Empty']);
|
||||
@@ -57,6 +61,7 @@ describe('run', () => {
|
||||
expect(e.message).toBe(`Couldn't find migration`);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns "Method implemented in the dialect driver" if no directus_migrations', async () => {
|
||||
tracker.on.select('directus_migrations').response([]);
|
||||
|
||||
@@ -65,6 +70,7 @@ describe('run', () => {
|
||||
expect(e.message).toBe('Nothing to downgrade');
|
||||
});
|
||||
});
|
||||
|
||||
it(`returns "Couldn't find migration" if an invalid migration object is supplied`, async () => {
|
||||
tracker.on.select('directus_migrations').response([
|
||||
{
|
||||
@@ -73,12 +79,14 @@ describe('run', () => {
|
||||
timestamp: '2020-00-32 11:36:56.471595-05',
|
||||
},
|
||||
]);
|
||||
|
||||
await run(db, 'down').catch((e: Error) => {
|
||||
expect(e).toBeInstanceOf(Error);
|
||||
expect(e.message).toBe(`Couldn't find migration`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when passed the argument latest', () => {
|
||||
it('returns "Nothing To downgrade" if no valid directus_migrations', async () => {
|
||||
tracker.on.select('directus_migrations').response(['Empty']);
|
||||
@@ -88,8 +96,10 @@ describe('run', () => {
|
||||
expect(e.message).toBe(`Method implemented in the dialect driver`);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns "Method implemented in the dialect driver" if no directus_migrations', async () => {
|
||||
tracker.on.select('directus_migrations').response([]);
|
||||
|
||||
await run(db, 'latest').catch((e: Error) => {
|
||||
expect(e).toBeInstanceOf(Error);
|
||||
expect(e.message).toBe('Method implemented in the dialect driver');
|
||||
|
||||
@@ -15,6 +15,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la
|
||||
let migrationFiles = await fse.readdir(__dirname);
|
||||
|
||||
const customMigrationsPath = path.resolve(env['EXTENSIONS_PATH'], 'migrations');
|
||||
|
||||
let customMigrationFiles =
|
||||
((await fse.pathExists(customMigrationsPath)) && (await fse.readdir(customMigrationsPath))) || [];
|
||||
|
||||
|
||||
@@ -335,6 +335,7 @@ async function getDBQuery(
|
||||
sortRecords.map((sortRecord) => {
|
||||
if (sortRecord.column.includes('.')) {
|
||||
const [alias, field] = sortRecord.column.split('.');
|
||||
|
||||
sortRecord.column = getColumn(knex, alias!, field!, false, schema, {
|
||||
originalCollectionName: getCollectionFromAlias(alias!, aliasMap),
|
||||
}) as any;
|
||||
@@ -498,6 +499,7 @@ function mergeWithParentItems(
|
||||
if (a[column] === b[column]) return 0;
|
||||
if (a[column] === null) return 1;
|
||||
if (b[column] === null) return -1;
|
||||
|
||||
if (order === 'asc') {
|
||||
return a[column] < b[column] ? -1 : 1;
|
||||
} else {
|
||||
|
||||
@@ -29,6 +29,7 @@ export class Emitter {
|
||||
context: EventContext
|
||||
): Promise<T> {
|
||||
const events = Array.isArray(event) ? event : [event];
|
||||
|
||||
const eventListeners = events.map((event) => ({
|
||||
event,
|
||||
listeners: this.filterEmitter.listeners(event) as FilterHandler<T>[],
|
||||
|
||||
@@ -443,6 +443,7 @@ function processValues(env: Record<string, any>) {
|
||||
|
||||
if (key.length > 5 && key.endsWith('_FILE')) {
|
||||
newKey = key.slice(0, -5);
|
||||
|
||||
if (allowedEnvironmentVars.some((pattern) => pattern.test(newKey as string))) {
|
||||
if (newKey in env && !(newKey in defaults && env[newKey] === defaults[newKey])) {
|
||||
throw new Error(
|
||||
|
||||
@@ -171,6 +171,7 @@ class ExtensionManager {
|
||||
const added = this.extensions.filter(
|
||||
(extension) => !prevExtensions.some((prevExtension) => extension.path === prevExtension.path)
|
||||
);
|
||||
|
||||
const removed = prevExtensions.filter(
|
||||
(prevExtension) => !this.extensions.some((extension) => prevExtension.path === extension.path)
|
||||
);
|
||||
@@ -356,6 +357,7 @@ class ExtensionManager {
|
||||
|
||||
private async generateExtensionBundle(): Promise<string | null> {
|
||||
const sharedDepsMapping = await this.getSharedDepsMapping(APP_SHARED_DEPS);
|
||||
|
||||
const internalImports = Object.entries(sharedDepsMapping).map(([name, path]) => ({
|
||||
find: name,
|
||||
replacement: path,
|
||||
@@ -370,6 +372,7 @@ class ExtensionManager {
|
||||
makeAbsoluteExternalsRelative: false,
|
||||
plugins: [virtual({ entry: entrypoint }), alias({ entries: internalImports }), nodeResolve({ browser: true })],
|
||||
});
|
||||
|
||||
const { output } = await bundle.generate({ format: 'es', compact: true });
|
||||
|
||||
for (const out of output) {
|
||||
@@ -470,6 +473,7 @@ class ExtensionManager {
|
||||
for (const operation of [...internalOperations, ...operations]) {
|
||||
try {
|
||||
const operationPath = path.resolve(operation.path, operation.entrypoint.api!);
|
||||
|
||||
const operationInstance: OperationApiConfig | { default: OperationApiConfig } = await import(
|
||||
`file://${operationPath}`
|
||||
);
|
||||
|
||||
@@ -181,6 +181,7 @@ class FlowManager {
|
||||
);
|
||||
|
||||
events.forEach((event) => emitter.onFilter(event, handler));
|
||||
|
||||
this.triggerHandlers.push({
|
||||
id: flow.id,
|
||||
events: events.map((event) => ({ type: 'filter', name: event, handler })),
|
||||
@@ -194,6 +195,7 @@ class FlowManager {
|
||||
});
|
||||
|
||||
events.forEach((event) => emitter.onAction(event, handler));
|
||||
|
||||
this.triggerHandlers.push({
|
||||
id: flow.id,
|
||||
events: events.map((event) => ({ type: 'action', name: event, handler })),
|
||||
|
||||
@@ -11,6 +11,7 @@ vi.mock('./env', async () => {
|
||||
LOG_LEVEL: 'info',
|
||||
LOG_STYLE: 'raw',
|
||||
};
|
||||
|
||||
return {
|
||||
default: MOCK_ENV,
|
||||
getEnv: () => MOCK_ENV,
|
||||
@@ -41,6 +42,7 @@ afterEach(() => {
|
||||
describe('req.headers.authorization', () => {
|
||||
test('Should redact bearer token in Authorization header', () => {
|
||||
const instance = pino(httpLoggerOptions, stream);
|
||||
|
||||
instance.info({
|
||||
req: {
|
||||
headers: {
|
||||
@@ -48,6 +50,7 @@ describe('req.headers.authorization', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(logOutput.mock.calls[0][0]).toMatchObject({
|
||||
req: {
|
||||
headers: {
|
||||
@@ -61,6 +64,7 @@ describe('req.headers.authorization', () => {
|
||||
describe('req.headers.cookie', () => {
|
||||
test('Should redact refresh token when there is only one entry', () => {
|
||||
const instance = pino(httpLoggerOptions, stream);
|
||||
|
||||
instance.info({
|
||||
req: {
|
||||
headers: {
|
||||
@@ -68,6 +72,7 @@ describe('req.headers.cookie', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(logOutput.mock.calls[0][0]).toMatchObject({
|
||||
req: {
|
||||
headers: {
|
||||
@@ -79,6 +84,7 @@ describe('req.headers.cookie', () => {
|
||||
|
||||
test('Should redact refresh token with multiple entries', () => {
|
||||
const instance = pino(httpLoggerOptions, stream);
|
||||
|
||||
instance.info({
|
||||
req: {
|
||||
headers: {
|
||||
@@ -86,6 +92,7 @@ describe('req.headers.cookie', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(logOutput.mock.calls[0][0]).toMatchObject({
|
||||
req: {
|
||||
headers: {
|
||||
@@ -99,6 +106,7 @@ describe('req.headers.cookie', () => {
|
||||
describe('res.headers', () => {
|
||||
test('Should redact refresh token when there is only one entry', () => {
|
||||
const instance = pino(httpLoggerOptions, stream);
|
||||
|
||||
instance.info({
|
||||
res: {
|
||||
headers: {
|
||||
@@ -106,6 +114,7 @@ describe('res.headers', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(logOutput.mock.calls[0][0]).toMatchObject({
|
||||
res: {
|
||||
headers: {
|
||||
@@ -117,6 +126,7 @@ describe('res.headers', () => {
|
||||
|
||||
test('Should redact refresh token with multiple entries', () => {
|
||||
const instance = pino(httpLoggerOptions, stream);
|
||||
|
||||
instance.info({
|
||||
res: {
|
||||
headers: {
|
||||
@@ -129,6 +139,7 @@ describe('res.headers', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(logOutput.mock.calls[0][0]).toMatchObject({
|
||||
res: {
|
||||
headers: {
|
||||
|
||||
@@ -33,6 +33,7 @@ if (env['LOG_STYLE'] !== 'raw') {
|
||||
sync: true,
|
||||
},
|
||||
};
|
||||
|
||||
httpLoggerOptions.transport = {
|
||||
target: 'pino-http-print',
|
||||
options: {
|
||||
@@ -85,6 +86,7 @@ if (loggerEnvConfig['levels']) {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
httpLoggerOptions.formatters = {
|
||||
level(label: string, number: any) {
|
||||
return {
|
||||
|
||||
@@ -54,6 +54,7 @@ export default function getMailer(): Transporter {
|
||||
} as Record<string, unknown>);
|
||||
} else if (transportName === 'mailgun') {
|
||||
const mg = require('nodemailer-mailgun-transport');
|
||||
|
||||
transporter = nodemailer.createTransport(
|
||||
mg({
|
||||
auth: {
|
||||
@@ -65,6 +66,7 @@ export default function getMailer(): Transporter {
|
||||
);
|
||||
} else if (transportName === 'sendgrid') {
|
||||
const sg = require('nodemailer-sendgrid');
|
||||
|
||||
transporter = nodemailer.createTransport(
|
||||
sg({
|
||||
apiKey: env['EMAIL_SENDGRID_API_KEY'],
|
||||
|
||||
@@ -10,10 +10,12 @@ import { InvalidCredentialsException } from '../exceptions/invalid-credentials.j
|
||||
import { handler } from './authenticate.js';
|
||||
|
||||
vi.mock('../../src/database');
|
||||
|
||||
vi.mock('../../src/env', () => {
|
||||
const MOCK_ENV = {
|
||||
SECRET: 'test',
|
||||
};
|
||||
|
||||
return {
|
||||
default: MOCK_ENV,
|
||||
getEnv: () => MOCK_ENV,
|
||||
@@ -29,6 +31,7 @@ test('Short-circuits when authenticate filter is used', async () => {
|
||||
ip: '127.0.0.1',
|
||||
get: vi.fn(),
|
||||
} as unknown as Request;
|
||||
|
||||
const res = {} as Response;
|
||||
const next = vi.fn();
|
||||
|
||||
@@ -56,6 +59,7 @@ test('Uses default public accountability when no token is given', async () => {
|
||||
}
|
||||
}),
|
||||
} as unknown as Request;
|
||||
|
||||
const res = {} as Response;
|
||||
const next = vi.fn();
|
||||
|
||||
@@ -80,10 +84,12 @@ test('Sets accountability to payload contents if valid token is passed', async (
|
||||
const userID = '3fac3c02-607f-4438-8d6e-6b8b25109b52';
|
||||
const roleID = '38269fc6-6eb6-475a-93cb-479d97f73039';
|
||||
const share = 'ca0ad005-f4ad-4bfe-b428-419ee8784790';
|
||||
|
||||
const shareScope = {
|
||||
collection: 'articles',
|
||||
item: 15,
|
||||
};
|
||||
|
||||
const appAccess = true;
|
||||
const adminAccess = false;
|
||||
|
||||
@@ -114,6 +120,7 @@ test('Sets accountability to payload contents if valid token is passed', async (
|
||||
}),
|
||||
token,
|
||||
} as unknown as Request;
|
||||
|
||||
const res = {} as Response;
|
||||
const next = vi.fn();
|
||||
|
||||
@@ -189,6 +196,7 @@ test('Throws InvalidCredentialsException when static token is used, but user doe
|
||||
}),
|
||||
token: 'static-token',
|
||||
} as unknown as Request;
|
||||
|
||||
const res = {} as Response;
|
||||
const next = vi.fn();
|
||||
|
||||
@@ -211,6 +219,7 @@ test('Sets accountability to user information when static token is used', async
|
||||
}),
|
||||
token: 'static-token',
|
||||
} as unknown as Request;
|
||||
|
||||
const res = {} as Response;
|
||||
const next = vi.fn();
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ function validateConfiguration() {
|
||||
|
||||
const globalPointsPerSec =
|
||||
Number(env['RATE_LIMITER_GLOBAL_POINTS']) / Math.max(Number(env['RATE_LIMITER_GLOBAL_DURATION']), 1);
|
||||
|
||||
const regularPointsPerSec = Number(env['RATE_LIMITER_POINTS']) / Math.max(Number(env['RATE_LIMITER_DURATION']), 1);
|
||||
|
||||
if (globalPointsPerSec <= regularPointsPerSec) {
|
||||
|
||||
@@ -55,6 +55,7 @@ test(`Short circuits on Array body in update/delete use`, async () => {
|
||||
|
||||
test(`Sets sanitizedQuery based on body.query in read operations`, async () => {
|
||||
mockRequest.method = 'SEARCH';
|
||||
|
||||
mockRequest.body = {
|
||||
query: {
|
||||
sort: 'id',
|
||||
@@ -70,6 +71,7 @@ test(`Sets sanitizedQuery based on body.query in read operations`, async () => {
|
||||
|
||||
test(`Doesn't allow both query and keys in a batch delete`, async () => {
|
||||
mockRequest.method = 'DELETE';
|
||||
|
||||
mockRequest.body = {
|
||||
keys: [1, 2, 3],
|
||||
query: { filter: {} },
|
||||
@@ -83,6 +85,7 @@ test(`Doesn't allow both query and keys in a batch delete`, async () => {
|
||||
|
||||
test(`Requires 'data' on batch update`, async () => {
|
||||
mockRequest.method = 'PATCH';
|
||||
|
||||
mockRequest.body = {
|
||||
keys: [1, 2, 3],
|
||||
query: { filter: {} },
|
||||
|
||||
@@ -9,6 +9,7 @@ describe('Operations / Condition', () => {
|
||||
_eq: true,
|
||||
},
|
||||
};
|
||||
|
||||
const data = {
|
||||
status: true,
|
||||
};
|
||||
@@ -22,6 +23,7 @@ describe('Operations / Condition', () => {
|
||||
_eq: true,
|
||||
},
|
||||
};
|
||||
|
||||
const data = {
|
||||
status: false,
|
||||
};
|
||||
@@ -42,6 +44,7 @@ describe('Operations / Condition', () => {
|
||||
_eq: true,
|
||||
},
|
||||
};
|
||||
|
||||
const data = {};
|
||||
|
||||
expect.assertions(2); // ensure catch block is reached
|
||||
|
||||
@@ -96,6 +96,7 @@ describe('Operations / Item Delete', () => {
|
||||
|
||||
test('should emit events for deleteOne when true', async () => {
|
||||
const key = 1;
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, key, emitEvents: true } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
@@ -106,6 +107,7 @@ describe('Operations / Item Delete', () => {
|
||||
|
||||
test.each([undefined, false])('should not emit events for deleteOne when %s', async (emitEvents) => {
|
||||
const key = 1;
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, key, emitEvents } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
@@ -127,6 +129,7 @@ describe('Operations / Item Delete', () => {
|
||||
|
||||
test('should emit events for deleteMany when true', async () => {
|
||||
const keys = [1, 2, 3];
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, key: keys, emitEvents: true } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
@@ -137,6 +140,7 @@ describe('Operations / Item Delete', () => {
|
||||
|
||||
test.each([undefined, false])('should not emit events for deleteMany when %s', async (emitEvents) => {
|
||||
const keys = [1, 2, 3];
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, key: keys, emitEvents } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
|
||||
@@ -96,6 +96,7 @@ describe('Operations / Item Read', () => {
|
||||
|
||||
test('should emit events for readOne when true', async () => {
|
||||
const key = 1;
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, key, emitEvents: true } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
@@ -106,6 +107,7 @@ describe('Operations / Item Read', () => {
|
||||
|
||||
test.each([undefined, false])('should not emit events for readOne when %s', async (emitEvents) => {
|
||||
const key = 1;
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, key, emitEvents } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
@@ -127,6 +129,7 @@ describe('Operations / Item Read', () => {
|
||||
|
||||
test('should emit events for readMany when true', async () => {
|
||||
const keys = [1, 2, 3];
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, key: keys, emitEvents: true } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
@@ -137,6 +140,7 @@ describe('Operations / Item Read', () => {
|
||||
|
||||
test.each([undefined, false])('should not emit events for readMany when %s', async (emitEvents) => {
|
||||
const keys = [1, 2, 3];
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, key: keys, emitEvents } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
|
||||
@@ -59,6 +59,7 @@ describe('Operations / Item Update', () => {
|
||||
|
||||
test.each([undefined, []])('should call updateByQuery with correct query when key is $payload', async (key) => {
|
||||
const query = { limit: -1 };
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, payload: testPayload, query, key } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
@@ -71,6 +72,7 @@ describe('Operations / Item Update', () => {
|
||||
|
||||
test('should emit events for updateByQuery when true', async () => {
|
||||
const query = { limit: -1 };
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, payload: testPayload, query, emitEvents: true } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
@@ -83,6 +85,7 @@ describe('Operations / Item Update', () => {
|
||||
|
||||
test.each([undefined, false])('should not emit events for updateByQuery when %s', async (emitEvents) => {
|
||||
const query = { limit: -1 };
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, payload: testPayload, query, emitEvents } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
@@ -106,6 +109,7 @@ describe('Operations / Item Update', () => {
|
||||
|
||||
test('should emit events for updateOne when true', async () => {
|
||||
const key = 1;
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, payload: testPayload, key, emitEvents: true } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
@@ -116,6 +120,7 @@ describe('Operations / Item Update', () => {
|
||||
|
||||
test.each([undefined, false])('should not emit events for updateOne when %s', async (emitEvents) => {
|
||||
const key = 1;
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, payload: testPayload, key: key, emitEvents } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
@@ -137,6 +142,7 @@ describe('Operations / Item Update', () => {
|
||||
|
||||
test('should emit events for updateMany when true', async () => {
|
||||
const keys = [1, 2, 3];
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, payload: testPayload, key: keys, emitEvents: true } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
@@ -147,6 +153,7 @@ describe('Operations / Item Update', () => {
|
||||
|
||||
test.each([undefined, false])('should not emit events for updateMany when %s', async (emitEvents) => {
|
||||
const keys = [1, 2, 3];
|
||||
|
||||
await config.handler(
|
||||
{ collection: testCollection, payload: testPayload, key: keys, emitEvents } as any,
|
||||
{ accountability: testAccountability, getSchema } as any
|
||||
|
||||
@@ -17,6 +17,7 @@ export default defineOperationApi<Options>({
|
||||
// If 'body' is of type object/undefined (happens when body consists solely of a placeholder)
|
||||
// convert it to JSON string
|
||||
const safeBody = typeof body !== 'string' ? JSON.stringify(body) : body;
|
||||
|
||||
await mailService.send({
|
||||
html: type === 'wysiwyg' ? safeBody : md(safeBody),
|
||||
to,
|
||||
|
||||
@@ -43,6 +43,7 @@ export default defineOperationApi<Options>({
|
||||
message: messageString,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await notificationsService.createMany(payload);
|
||||
|
||||
return result;
|
||||
|
||||
@@ -37,10 +37,12 @@ test('no headers configured', async () => {
|
||||
|
||||
test('headers array is converted to object', async () => {
|
||||
const body = 'body';
|
||||
|
||||
const headers = [
|
||||
{ header: 'header1', value: 'value1' },
|
||||
{ header: 'header2', value: 'value2' },
|
||||
];
|
||||
|
||||
await config.handler({ url, method, body, headers }, {} as any);
|
||||
|
||||
expect(axiosDefault).toHaveBeenCalledWith(
|
||||
|
||||
@@ -48,6 +48,7 @@ test(`Checks against IPs of local networkInterfaces if IP deny list contains 0.0
|
||||
|
||||
test(`Throws error if IP address matches resolved localhost IP`, async () => {
|
||||
vi.mocked(getEnv).mockReturnValue({ IMPORT_IP_DENY_LIST: ['0.0.0.0'] });
|
||||
|
||||
vi.mocked(os.networkInterfaces).mockReturnValue({
|
||||
fa0: undefined,
|
||||
lo0: [
|
||||
|
||||
@@ -347,10 +347,12 @@ export class AuthenticationService {
|
||||
if (record.share_id) {
|
||||
tokenPayload.share = record.share_id;
|
||||
tokenPayload.role = record.share_role;
|
||||
|
||||
tokenPayload.share_scope = {
|
||||
collection: record.share_collection,
|
||||
item: record.share_item,
|
||||
};
|
||||
|
||||
tokenPayload.app_access = false;
|
||||
tokenPayload.admin_access = false;
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export class AuthorizationService {
|
||||
this.knex = options.knex || getDatabase();
|
||||
this.accountability = options.accountability || null;
|
||||
this.schema = options.schema;
|
||||
|
||||
this.payloadService = new PayloadService('directus_permissions', {
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
@@ -236,6 +237,7 @@ export class AuthorizationService {
|
||||
parentCollection,
|
||||
parentField
|
||||
);
|
||||
|
||||
result = mergeRequiredFieldPermissions(result, requiredPermissions);
|
||||
}
|
||||
}
|
||||
@@ -306,6 +308,7 @@ export class AuthorizationService {
|
||||
|
||||
parentCollection =
|
||||
relation.related_collection === parentCollection ? relation.collection : relation.related_collection!;
|
||||
|
||||
(result[parentCollection] || (result[parentCollection] = new Set())).add(filterKey);
|
||||
}
|
||||
|
||||
@@ -324,6 +327,7 @@ export class AuthorizationService {
|
||||
parentCollection,
|
||||
filterKey
|
||||
);
|
||||
|
||||
result = mergeRequiredFieldPermissions(result, requiredPermissions);
|
||||
}
|
||||
}
|
||||
@@ -335,6 +339,7 @@ export class AuthorizationService {
|
||||
parentCollection,
|
||||
filterKey
|
||||
);
|
||||
|
||||
result = mergeRequiredFieldPermissions(result, requiredPermissions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ export class CollectionsService {
|
||||
});
|
||||
|
||||
const fieldPayloads = payload.fields!.filter((field) => field.meta).map((field) => field.meta) as FieldMeta[];
|
||||
|
||||
await fieldItemsService.createMany(fieldPayloads, {
|
||||
bypassEmitAction: (params) =>
|
||||
opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
||||
@@ -213,6 +214,7 @@ export class CollectionsService {
|
||||
autoPurgeSystemCache: false,
|
||||
bypassEmitAction: (params) => nestedActionEvents.push(params),
|
||||
});
|
||||
|
||||
collectionNames.push(name);
|
||||
}
|
||||
|
||||
@@ -456,6 +458,7 @@ export class CollectionsService {
|
||||
bypassEmitAction: (params) =>
|
||||
opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
||||
});
|
||||
|
||||
collectionKeys.push(payload[collectionKey]);
|
||||
}
|
||||
});
|
||||
@@ -636,6 +639,7 @@ export class CollectionsService {
|
||||
const newAllowedCollections = relation
|
||||
.meta!.one_allowed_collections!.filter((collection) => collectionKey !== collection)
|
||||
.join(',');
|
||||
|
||||
await trx('directus_relations')
|
||||
.update({ one_allowed_collections: newAllowedCollections })
|
||||
.where({ id: relation.meta!.id });
|
||||
|
||||
@@ -196,6 +196,7 @@ export class FieldsService {
|
||||
});
|
||||
|
||||
if (!permissions || !permissions.fields) throw new ForbiddenException();
|
||||
|
||||
if (permissions.fields.includes('*') === false) {
|
||||
const allowedFields = permissions.fields;
|
||||
if (allowedFields.includes(field) === false) throw new ForbiddenException();
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('Integration Tests', () => {
|
||||
knex: db,
|
||||
schema: { collections: {}, relations: [] },
|
||||
});
|
||||
|
||||
superCreateOne = vi.spyOn(ItemsService.prototype, 'createOne').mockReturnValue(Promise.resolve(1));
|
||||
});
|
||||
|
||||
|
||||
@@ -271,6 +271,7 @@ export class FilesService extends ItemsService {
|
||||
|
||||
try {
|
||||
const axios = await getAxios();
|
||||
|
||||
fileResponse = await axios.get<Readable>(encodeURL(importURL), {
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
@@ -204,6 +204,7 @@ export class GraphQLService {
|
||||
|
||||
const scopeFilter = (collection: SchemaOverview['collections'][string]) => {
|
||||
if (this.scope === 'items' && collection.collection.startsWith('directus_') === true) return false;
|
||||
|
||||
if (this.scope === 'system') {
|
||||
if (collection.collection.startsWith('directus_') === false) return false;
|
||||
if (SYSTEM_DENY_LIST.includes(collection.collection)) return false;
|
||||
@@ -239,6 +240,7 @@ export class GraphQLService {
|
||||
acc[`${collectionName}_by_id`] = ReadCollectionTypes[collection.collection]!.getResolver(
|
||||
`${collection.collection}_by_id`
|
||||
);
|
||||
|
||||
acc[`${collectionName}_aggregated`] = ReadCollectionTypes[collection.collection]!.getResolver(
|
||||
`${collection.collection}_aggregated`
|
||||
);
|
||||
@@ -264,12 +266,15 @@ export class GraphQLService {
|
||||
.filter((collection) => READ_ONLY.includes(collection.collection) === false)
|
||||
.reduce((acc, collection) => {
|
||||
const collectionName = this.scope === 'items' ? collection.collection : collection.collection.substring(9);
|
||||
|
||||
acc[`create_${collectionName}_items`] = CreateCollectionTypes[collection.collection]!.getResolver(
|
||||
`create_${collection.collection}_items`
|
||||
);
|
||||
|
||||
acc[`create_${collectionName}_item`] = CreateCollectionTypes[collection.collection]!.getResolver(
|
||||
`create_${collection.collection}_item`
|
||||
);
|
||||
|
||||
return acc;
|
||||
}, {} as ObjectTypeComposerFieldConfigAsObjectDefinition<any, any>)
|
||||
);
|
||||
@@ -893,6 +898,7 @@ export class GraphQLService {
|
||||
type: GraphQLFloat,
|
||||
description: field.note,
|
||||
};
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -1115,6 +1121,7 @@ export class GraphQLService {
|
||||
}
|
||||
} else if (relation.meta?.one_allowed_collections) {
|
||||
ReadableCollectionFilterTypes[relation.collection]?.removeField('item');
|
||||
|
||||
for (const collection of relation.meta.one_allowed_collections) {
|
||||
ReadableCollectionFilterTypes[relation.collection]?.addFields({
|
||||
[`item__${collection}`]: ReadableCollectionFilterTypes[collection]!,
|
||||
@@ -1997,6 +2004,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
scope: args['scope'] ?? 'items',
|
||||
});
|
||||
|
||||
return service.getSchema('sdl');
|
||||
},
|
||||
},
|
||||
@@ -2011,6 +2019,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
return await service.serverInfo();
|
||||
},
|
||||
},
|
||||
@@ -2021,6 +2030,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
return await service.health();
|
||||
},
|
||||
},
|
||||
@@ -2065,6 +2075,7 @@ export class GraphQLService {
|
||||
accountability: accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const result = await authenticationService.login(DEFAULT_AUTH_PROVIDER, args, args?.otp);
|
||||
|
||||
if (args['mode'] === 'cookie') {
|
||||
@@ -2105,6 +2116,7 @@ export class GraphQLService {
|
||||
accountability: accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const currentRefreshToken = args['refresh_token'] || req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
|
||||
|
||||
if (!currentRefreshToken) {
|
||||
@@ -2150,6 +2162,7 @@ export class GraphQLService {
|
||||
accountability: accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const currentRefreshToken = args['refresh_token'] || req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
|
||||
|
||||
if (!currentRefreshToken) {
|
||||
@@ -2224,14 +2237,17 @@ export class GraphQLService {
|
||||
},
|
||||
resolve: async (_, args) => {
|
||||
if (!this.accountability?.user) return null;
|
||||
|
||||
const service = new TFAService({
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const authService = new AuthenticationService({
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await authService.verifyPassword(this.accountability.user, args['password']);
|
||||
const { url, secret } = await service.generateTFA(this.accountability.user);
|
||||
return { secret, otpauth_url: url };
|
||||
@@ -2245,6 +2261,7 @@ export class GraphQLService {
|
||||
},
|
||||
resolve: async (_, args) => {
|
||||
if (!this.accountability?.user) return null;
|
||||
|
||||
const service = new TFAService({
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
@@ -2261,10 +2278,12 @@ export class GraphQLService {
|
||||
},
|
||||
resolve: async (_, args) => {
|
||||
if (!this.accountability?.user) return null;
|
||||
|
||||
const service = new TFAService({
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const otpValid = await service.verifyOTP(this.accountability.user, args['otp']);
|
||||
|
||||
if (otpValid === false) {
|
||||
@@ -2321,6 +2340,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const { item, to } = args;
|
||||
await service.sort(args['collection'], { item, to });
|
||||
return true;
|
||||
@@ -2336,6 +2356,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await service.revert(args['revision']);
|
||||
return true;
|
||||
},
|
||||
@@ -2366,6 +2387,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await service.acceptInvite(args['token'], args['password']);
|
||||
return true;
|
||||
},
|
||||
@@ -2474,6 +2496,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
return await service.readAll();
|
||||
},
|
||||
},
|
||||
@@ -2502,6 +2525,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
return await service.readOne(args['collection'], args['field']);
|
||||
},
|
||||
},
|
||||
@@ -2575,6 +2599,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
return await service.readOne(args['collection'], args['field']);
|
||||
},
|
||||
},
|
||||
@@ -2599,6 +2624,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const collectionKey = await collectionsService.createOne(args['data']);
|
||||
return await collectionsService.readOne(collectionKey);
|
||||
},
|
||||
@@ -2616,6 +2642,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const collectionKey = await collectionsService.updateOne(args['collection'], args['data']);
|
||||
return await collectionsService.readOne(collectionKey);
|
||||
},
|
||||
@@ -2635,6 +2662,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await collectionsService.deleteOne(args['collection']);
|
||||
return { collection: args['collection'] };
|
||||
},
|
||||
@@ -2653,6 +2681,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await service.createField(args['collection'], args['data']);
|
||||
return await service.readOne(args['collection'], args['data'].field);
|
||||
},
|
||||
@@ -2669,10 +2698,12 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await service.updateField(args['collection'], {
|
||||
...args['data'],
|
||||
field: args['field'],
|
||||
});
|
||||
|
||||
return await service.readOne(args['collection'], args['data'].field);
|
||||
},
|
||||
},
|
||||
@@ -2693,6 +2724,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await service.deleteField(args['collection'], args['field']);
|
||||
const { collection, field } = args;
|
||||
return { collection, field };
|
||||
@@ -2750,6 +2782,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await relationsService.deleteOne(args['collection'], args['field']);
|
||||
return { collection: args['collection'], field: args['field'] };
|
||||
},
|
||||
@@ -2764,10 +2797,12 @@ export class GraphQLService {
|
||||
resolve: async (_, args, __, info) => {
|
||||
if (!this.accountability?.user) return null;
|
||||
const service = new UsersService({ schema: this.schema, accountability: this.accountability });
|
||||
|
||||
const selections = this.replaceFragmentsInSelections(
|
||||
info.fieldNodes[0]?.selectionSet?.selections,
|
||||
info.fragments
|
||||
);
|
||||
|
||||
const query = this.getQuery(args, selections || [], info.variableValues);
|
||||
|
||||
return await service.readOne(this.accountability.user, query);
|
||||
@@ -2785,6 +2820,7 @@ export class GraphQLService {
|
||||
},
|
||||
resolve: async (_, args, __, info) => {
|
||||
if (!this.accountability?.user) return null;
|
||||
|
||||
const service = new UsersService({
|
||||
schema: this.schema,
|
||||
accountability: this.accountability,
|
||||
@@ -2797,6 +2833,7 @@ export class GraphQLService {
|
||||
info.fieldNodes[0]?.selectionSet?.selections,
|
||||
info.fragments
|
||||
);
|
||||
|
||||
const query = this.getQuery(args, selections || [], info.variableValues);
|
||||
|
||||
return await service.readOne(this.accountability.user, query);
|
||||
@@ -2837,6 +2874,7 @@ export class GraphQLService {
|
||||
info.fieldNodes[0]?.selectionSet?.selections,
|
||||
info.fragments
|
||||
);
|
||||
|
||||
const query = this.getQuery(args, selections || [], info.variableValues);
|
||||
|
||||
return await service.readOne(primaryKey, query);
|
||||
@@ -2861,6 +2899,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const primaryKey = await service.updateOne(args['id'], { comment: args['comment'] });
|
||||
|
||||
if ('directus_activity' in ReadCollectionTypes) {
|
||||
@@ -2868,6 +2907,7 @@ export class GraphQLService {
|
||||
info.fieldNodes[0]?.selectionSet?.selections,
|
||||
info.fragments
|
||||
);
|
||||
|
||||
const query = this.getQuery(args, selections || [], info.variableValues);
|
||||
|
||||
return await service.readOne(primaryKey, query);
|
||||
@@ -2891,6 +2931,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await service.deleteOne(args['id']);
|
||||
return { id: args['id'] };
|
||||
},
|
||||
@@ -2913,6 +2954,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const primaryKey = await service.importOne(args['url'], args['data']);
|
||||
|
||||
if ('directus_files' in ReadCollectionTypes) {
|
||||
@@ -2920,6 +2962,7 @@ export class GraphQLService {
|
||||
info.fieldNodes[0]?.selectionSet?.selections,
|
||||
info.fragments
|
||||
);
|
||||
|
||||
const query = this.getQuery(args, selections || [], info.variableValues);
|
||||
return await service.readOne(primaryKey, query);
|
||||
}
|
||||
@@ -2944,6 +2987,7 @@ export class GraphQLService {
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await service.inviteUser(args['email'], args['role'], args['invite_url'] || null);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ export const GraphQLBigInt = new GraphQLScalarType({
|
||||
serialize(value) {
|
||||
if (!value) return value;
|
||||
if (typeof value === 'string') return value;
|
||||
|
||||
if (typeof value !== 'number') {
|
||||
throw new Error('Value must be a Number');
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import processError from './process-error.js';
|
||||
|
||||
describe('GraphQL processError util', () => {
|
||||
const sampleError = new GraphQLError('An error message', { path: ['test_collection'] });
|
||||
|
||||
const redactedError = {
|
||||
message: 'An unexpected error occurred.',
|
||||
locations: undefined,
|
||||
|
||||
@@ -358,6 +358,7 @@ export class ExportService {
|
||||
|
||||
if (format === 'csv') {
|
||||
if (input.length === 0) return '';
|
||||
|
||||
const parser = new CSVParser({
|
||||
transforms: [CSVTransforms.flatten({ separator: '.' })],
|
||||
header: options?.includeHeader !== false,
|
||||
|
||||
@@ -12,10 +12,12 @@ import { systemSchema, userSchema } from '../__utils__/schemas.js';
|
||||
|
||||
vi.mock('../env', async () => {
|
||||
const actual = (await vi.importActual('../env')) as { default: Record<string, any> };
|
||||
|
||||
const MOCK_ENV = {
|
||||
...actual.default,
|
||||
CACHE_AUTO_PURGE: true,
|
||||
};
|
||||
|
||||
return {
|
||||
default: MOCK_ENV,
|
||||
getEnv: () => MOCK_ENV,
|
||||
@@ -80,6 +82,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.insert.length).toBe(1);
|
||||
expect(tracker.history.insert[0]!.bindings).toStrictEqual([item.id, item.name]);
|
||||
|
||||
expect(tracker.history.insert[0]!.sql).toBe(
|
||||
`insert into "${table}" (${sqlFieldList(schemas[schema].schema, table)}) values (?, ?)`
|
||||
);
|
||||
@@ -122,10 +125,12 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readOne(rawItems[0]!.id, { fields: ['id', 'name'] });
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([rawItems[0]!.id, 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select ${sqlFieldFormatter(
|
||||
schemas[schema].schema,
|
||||
@@ -163,10 +168,12 @@ describe('Integration Tests', () => {
|
||||
},
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readOne(rawItems[0]!.id);
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([rawItems[0]!.id, 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select "${table}"."id" from "${table}" where ("${table}"."id" = ?) order by "${table}"."id" asc limit ?`
|
||||
);
|
||||
@@ -185,6 +192,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readOne(rawItems[0]!.id, {
|
||||
fields: ['id', 'name'],
|
||||
filter: { name: { _eq: 'something' } },
|
||||
@@ -192,6 +200,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual(['something', rawItems[0]!.id, 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select "${table}"."id", "${table}"."name" from "${table}" where "${table}"."name" = ? and "${table}"."id" = ? order by "${table}"."id" asc limit ?`
|
||||
);
|
||||
@@ -236,6 +245,7 @@ describe('Integration Tests', () => {
|
||||
},
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readOne(rawItems[0]!.id, {
|
||||
fields: ['id', 'name'],
|
||||
filter: { name: { _eq: 'something' } },
|
||||
@@ -243,6 +253,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual(['something', rawItems[0]!.id, 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select "${table}"."id", "${table}"."name" from "${table}" where ("${table}"."name" = ? and "${table}"."id" = ?) order by "${table}"."id" asc limit ?`
|
||||
);
|
||||
@@ -288,17 +299,20 @@ describe('Integration Tests', () => {
|
||||
},
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readOne(rawItems[0]!.id, {
|
||||
fields: ['id'],
|
||||
filter: { uploaded_by: { _in: ['b5a7dd0f-fc9f-4242-b331-83990990198f'] } },
|
||||
});
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([
|
||||
'b5a7dd0f-fc9f-4242-b331-83990990198f',
|
||||
rawItems[0]!.id,
|
||||
100,
|
||||
]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select "${table}"."id" from "${table}" where ("${table}"."uploaded_by" in (?) and "${table}"."id" = ?) order by "${table}"."id" asc limit ?`
|
||||
);
|
||||
@@ -364,17 +378,20 @@ describe('Integration Tests', () => {
|
||||
},
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readOne(rawItems[0]!.id, {
|
||||
fields: ['id'],
|
||||
filter: { uploaded_by: { _in: ['b5a7dd0f-fc9f-4242-b331-83990990198f'] } },
|
||||
});
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([
|
||||
'b5a7dd0f-fc9f-4242-b331-83990990198f',
|
||||
rawItems[0]!.id,
|
||||
100,
|
||||
]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select "${table}"."id" from "${table}" where ("${table}"."uploaded_by" in (?) and "${table}"."id" = ?) order by "${table}"."id" asc limit ?`
|
||||
);
|
||||
@@ -387,10 +404,12 @@ describe('Integration Tests', () => {
|
||||
'%s denies one item with filter from tables not as admin and has no field permissions',
|
||||
async (schema) => {
|
||||
let table = schemas[schema].tables[1];
|
||||
|
||||
const item = {
|
||||
id: 'd66ec139-2655-48c1-9d9a-4753f98a9ee7',
|
||||
uploaded_by: '6107c897-9182-40f7-b22e-4f044d1258d2',
|
||||
};
|
||||
|
||||
let itemsService = new ItemsService(table, {
|
||||
knex: db,
|
||||
accountability: { role: 'admin', admin: true },
|
||||
@@ -439,6 +458,7 @@ describe('Integration Tests', () => {
|
||||
expect(() =>
|
||||
itemsService.readOne(rawItems[0]!.id, { filter: { name: { _eq: 'something' } } })
|
||||
).rejects.toThrow("You don't have permission to access this.");
|
||||
|
||||
expect(tracker.history.select.length).toBe(0);
|
||||
}
|
||||
);
|
||||
@@ -465,6 +485,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readOne(rawItems[0]!.id, {
|
||||
fields: ['id', 'items.*'],
|
||||
deep: { items: { _filter: { title: { _eq: childItems[0]!.title } } } as NestedDeepQuery },
|
||||
@@ -472,17 +493,21 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(2);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([rawItems[0]!.id, 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select "${table}"."id" from "${table}" where "${table}"."id" = ? order by "${table}"."id" asc limit ?`
|
||||
);
|
||||
|
||||
expect(tracker.history.select[1]!.bindings).toStrictEqual([
|
||||
childItems[0]!.title,
|
||||
...rawItems.map((item) => item.id),
|
||||
25000,
|
||||
]);
|
||||
|
||||
expect(tracker.history.select[1]!.sql).toBe(
|
||||
`select "${childTable}"."id", "${childTable}"."title", "${childTable}"."uploaded_by" from "${childTable}" where "${childTable}"."title" = ? and "${childTable}"."uploaded_by" in (?, ?) order by "${childTable}"."id" asc limit ?`
|
||||
);
|
||||
|
||||
expect(response).toStrictEqual({ id: rawItems[0]!.id, items: childItems });
|
||||
});
|
||||
|
||||
@@ -555,6 +580,7 @@ describe('Integration Tests', () => {
|
||||
},
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readOne(rawItems[0]!.id, {
|
||||
fields: ['id', 'items.*'],
|
||||
deep: { items: { _filter: { title: { _eq: childItems[0]!.title } } } as NestedDeepQuery },
|
||||
@@ -562,17 +588,21 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(2);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([rawItems[0]!.id, 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select "${table}"."id" from "${table}" where ("${table}"."id" = ?) order by "${table}"."id" asc limit ?`
|
||||
);
|
||||
|
||||
expect(tracker.history.select[1]!.bindings).toStrictEqual([
|
||||
childItems[0]!.title,
|
||||
...rawItems.map((item) => item.id),
|
||||
25000,
|
||||
]);
|
||||
|
||||
expect(tracker.history.select[1]!.sql).toBe(
|
||||
`select "${childTable}"."id", "${childTable}"."title", "${childTable}"."uploaded_by" from "${childTable}" where ("${childTable}"."title" = ?) and "${childTable}"."uploaded_by" in (?, ?) order by "${childTable}"."id" asc limit ?`
|
||||
);
|
||||
|
||||
expect(response).toStrictEqual({ id: rawItems[0]!.id, items: childItems });
|
||||
}
|
||||
);
|
||||
@@ -653,6 +683,7 @@ describe('Integration Tests', () => {
|
||||
deep: { items: { _filter: { title: { _eq: childItems[0]!.title } } } as NestedDeepQuery },
|
||||
})
|
||||
).rejects.toThrow("You don't have permission to access this.");
|
||||
|
||||
expect(tracker.history.select.length).toBe(0);
|
||||
}
|
||||
);
|
||||
@@ -677,6 +708,7 @@ describe('Integration Tests', () => {
|
||||
expect(() => itemsService.readOne(rawItems[0]!.id)).rejects.toThrow(
|
||||
"You don't have permission to access this."
|
||||
);
|
||||
|
||||
expect(tracker.history.select.length).toBe(0);
|
||||
}
|
||||
);
|
||||
@@ -706,9 +738,11 @@ describe('Integration Tests', () => {
|
||||
},
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
expect(() => itemsService.readOne(rawItems[0]!.id)).rejects.toThrow(
|
||||
"You don't have permission to access this."
|
||||
);
|
||||
|
||||
expect(tracker.history.select.length).toBe(0);
|
||||
}
|
||||
);
|
||||
@@ -757,12 +791,14 @@ describe('Integration Tests', () => {
|
||||
},
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readOne(rawItems[0]!.id, {
|
||||
fields: ['count(items)'],
|
||||
});
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([rawItems[0]!.id, 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select (select count(*) from "${childTable}" where "uploaded_by" = "${table}"."id") AS "items_count", "${table}"."id" from "${table}" where ("${table}"."id" = ?) order by "${table}"."id" asc limit ?`
|
||||
);
|
||||
@@ -814,12 +850,14 @@ describe('Integration Tests', () => {
|
||||
},
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readOne(rawItems[0]!.id, {
|
||||
fields: ['count(items)'],
|
||||
});
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([rawItems[0]!.id, 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select (select count(*) from "${childTable}" where "uploaded_by" = "${table}"."id" and (("${childTable}"."title" like '%child%'))) AS "items_count", "${table}"."id" from "${table}" where ("${table}"."id" = ?) order by "${table}"."id" asc limit ?`
|
||||
);
|
||||
@@ -845,10 +883,12 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readMany([items[0]!.id, items[1]!.id], { fields: ['id', 'name'] });
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([items[0]!.id, items[1]!.id, 2]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select ${sqlFieldFormatter(
|
||||
schemas[schema].schema,
|
||||
@@ -870,6 +910,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readMany([], {
|
||||
fields: ['id', 'name'],
|
||||
filter: { id: { _eq: items[1]!.id } },
|
||||
@@ -877,6 +918,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([0, items[1]!.id, 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select ${sqlFieldFormatter(
|
||||
schemas[schema].schema,
|
||||
@@ -897,6 +939,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readMany([], {
|
||||
fields: ['id', 'name'],
|
||||
filter: { _or: [{ id: { _eq: items[1]!.id } }, { name: { _eq: items[1]!.name } }] },
|
||||
@@ -904,6 +947,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([0, items[1]!.id, items[1]!.name, 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select ${sqlFieldFormatter(
|
||||
schemas[schema].schema,
|
||||
@@ -929,6 +973,7 @@ describe('Integration Tests', () => {
|
||||
title: 'A new child item',
|
||||
uploaded_by: '6107c897-9182-40f7-b22e-4f044d1258d2',
|
||||
};
|
||||
|
||||
tracker.on.select(childTable).response([childItem]);
|
||||
tracker.on.update(childTable).response(childItem);
|
||||
|
||||
@@ -987,6 +1032,7 @@ describe('Integration Tests', () => {
|
||||
},
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
const response = await itemsService.updateOne(
|
||||
item.id,
|
||||
{
|
||||
@@ -997,21 +1043,29 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(4);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([item.id, 1]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select "${table}"."id", "${table}"."name" from "${table}" where (("${table}"."id" in (?))) order by "${table}"."id" asc limit ?`
|
||||
);
|
||||
|
||||
expect(tracker.history.select[1]!.bindings).toStrictEqual([item.id, 25000]);
|
||||
|
||||
expect(tracker.history.select[1]!.sql).toBe(
|
||||
`select "${childTable}"."uploaded_by", "${childTable}"."id" from "${childTable}" where "${childTable}"."uploaded_by" in (?) order by "${childTable}"."id" asc limit ?`
|
||||
);
|
||||
|
||||
expect(tracker.history.select[2]!.bindings).toStrictEqual([item.id, 1, 100]);
|
||||
|
||||
expect(tracker.history.select[2]!.sql).toBe(
|
||||
`select "${childTable}"."id" from "${childTable}" where ("${childTable}"."uploaded_by" = ? and 1 = ?) order by "${childTable}"."id" asc limit ?`
|
||||
);
|
||||
|
||||
expect(tracker.history.select[3]!.bindings).toStrictEqual([childItem.id, 1]);
|
||||
|
||||
expect(tracker.history.select[3]!.sql).toBe(
|
||||
`select "${childTable}"."id", "${childTable}"."title", "${childTable}"."uploaded_by" from "${childTable}" where (("${childTable}"."id" in (?))) order by "${childTable}"."id" asc limit ?`
|
||||
);
|
||||
|
||||
expect(tracker.history.update[0]!.bindings).toStrictEqual([null, childItem.id]);
|
||||
expect(tracker.history.update[0]!.sql).toBe(`update "${childTable}" set "uploaded_by" = ? where "id" in (?)`);
|
||||
|
||||
@@ -1093,6 +1147,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
await itemsService.readByQuery({
|
||||
fields: ['id', 'name'],
|
||||
filter: { name: { _eq: 'something' } },
|
||||
@@ -1100,6 +1155,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual(['something', 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select "${table}"."id", "${table}"."name" from "${table}" where "${table}"."name" = ? order by "${table}"."id" asc limit ?`
|
||||
);
|
||||
@@ -1116,6 +1172,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
await itemsService.readByQuery({
|
||||
fields: ['id', 'title'],
|
||||
filter: { uploaded_by: { name: { _eq: 'something' } } },
|
||||
@@ -1123,6 +1180,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual(['something', 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toMatch(
|
||||
new RegExp(
|
||||
`select "${otherTable}"."id", "${otherTable}"."title" from "${otherTable}" ` +
|
||||
@@ -1143,6 +1201,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
await itemsService.readByQuery({
|
||||
fields: ['id', 'name'],
|
||||
filter: { items: { title: { _eq: 'something' } } },
|
||||
@@ -1150,6 +1209,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual(['something', 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toMatch(
|
||||
new RegExp(
|
||||
`select "${table}"."id", "${table}"."name" from "${table}" inner join ` +
|
||||
@@ -1174,6 +1234,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
await itemsService.readByQuery({
|
||||
fields: ['id', 'name'],
|
||||
sort: ['name'],
|
||||
@@ -1181,6 +1242,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toBe(
|
||||
`select "${table}"."id", "${table}"."name" from "${table}" order by "${table}"."name" asc limit ?`
|
||||
);
|
||||
@@ -1197,6 +1259,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
await itemsService.readByQuery({
|
||||
fields: ['id', 'title'],
|
||||
sort: ['uploaded_by.name'],
|
||||
@@ -1204,6 +1267,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toMatch(
|
||||
new RegExp(
|
||||
`select "${otherTable}"."id", "${otherTable}"."title" from "${otherTable}" ` +
|
||||
@@ -1223,6 +1287,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: schemas[schema].schema,
|
||||
});
|
||||
|
||||
await itemsService.readByQuery({
|
||||
fields: ['id', 'name'],
|
||||
sort: ['items.title'],
|
||||
@@ -1230,6 +1295,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
expect(tracker.history.select[0]!.bindings).toStrictEqual([100, 1, 100]);
|
||||
|
||||
expect(tracker.history.select[0]!.sql).toMatch(
|
||||
new RegExp(
|
||||
`select "${table}"."id", "${table}"."name" from "${table}" ` +
|
||||
@@ -1378,6 +1444,7 @@ describe('Integration Tests', () => {
|
||||
accountability: { role: 'admin', admin: true },
|
||||
schema: testSchema,
|
||||
});
|
||||
|
||||
const response = await itemsService.readSingleton({ fields: ['*'] });
|
||||
|
||||
expect(tracker.history.select.length).toBe(1);
|
||||
|
||||
@@ -60,6 +60,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
return {
|
||||
trackMutations(count: number) {
|
||||
mutationCount += count;
|
||||
|
||||
if (mutationCount > maxCount) {
|
||||
throw new InvalidPayloadException(`Exceeded max batch mutation limit of ${maxCount}.`);
|
||||
}
|
||||
@@ -92,6 +93,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
*/
|
||||
async createOne(data: Partial<Item>, opts: MutationOptions = {}): Promise<PrimaryKey> {
|
||||
if (!opts.mutationTracker) opts.mutationTracker = this.createMutationTracker();
|
||||
|
||||
if (!opts.bypassLimits) {
|
||||
opts.mutationTracker.trackMutations(1);
|
||||
}
|
||||
@@ -101,6 +103,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
|
||||
const primaryKeyField = this.schema.collections[this.collection]!.primary;
|
||||
const fields = Object.keys(this.schema.collections[this.collection]!.fields);
|
||||
|
||||
const aliases = Object.values(this.schema.collections[this.collection]!.fields)
|
||||
.filter((field) => field.alias === true)
|
||||
.map((field) => field.field);
|
||||
@@ -159,6 +162,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
revisions: revisionsM2O,
|
||||
nestedActionEvents: nestedActionEventsM2O,
|
||||
} = await payloadService.processM2O(payloadWithPresets, opts);
|
||||
|
||||
const {
|
||||
payload: payloadWithA2O,
|
||||
revisions: revisionsA2O,
|
||||
@@ -324,6 +328,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
bypassEmitAction: (params) => nestedActionEvents.push(params),
|
||||
mutationTracker: opts.mutationTracker,
|
||||
});
|
||||
|
||||
primaryKeys.push(primaryKey);
|
||||
}
|
||||
|
||||
@@ -538,6 +543,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
*/
|
||||
async updateMany(keys: PrimaryKey[], data: Partial<Item>, opts: MutationOptions = {}): Promise<PrimaryKey[]> {
|
||||
if (!opts.mutationTracker) opts.mutationTracker = this.createMutationTracker();
|
||||
|
||||
if (!opts.bypassLimits) {
|
||||
opts.mutationTracker.trackMutations(keys.length);
|
||||
}
|
||||
@@ -549,6 +555,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
validateKeys(this.schema, this.collection, primaryKeyField, keys);
|
||||
|
||||
const fields = Object.keys(this.schema.collections[this.collection]!.fields);
|
||||
|
||||
const aliases = Object.values(this.schema.collections[this.collection]!.fields)
|
||||
.filter((field) => field.alias === true)
|
||||
.map((field) => field.field);
|
||||
@@ -610,6 +617,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
revisions: revisionsM2O,
|
||||
nestedActionEvents: nestedActionEventsM2O,
|
||||
} = await payloadService.processM2O(payloadWithPresets, opts);
|
||||
|
||||
const {
|
||||
payload: payloadWithA2O,
|
||||
revisions: revisionsA2O,
|
||||
@@ -638,6 +646,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
key,
|
||||
opts
|
||||
);
|
||||
|
||||
childrenRevisions.push(...revisions);
|
||||
nestedActionEvents.push(...nestedActionEventsO2M);
|
||||
}
|
||||
@@ -835,6 +844,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
*/
|
||||
async deleteMany(keys: PrimaryKey[], opts: MutationOptions = {}): Promise<PrimaryKey[]> {
|
||||
if (!opts.mutationTracker) opts.mutationTracker = this.createMutationTracker();
|
||||
|
||||
if (!opts.bypassLimits) {
|
||||
opts.mutationTracker.trackMutations(keys.length);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export class NotificationsService extends ItemsService {
|
||||
const user = await this.usersService.readOne(data.recipient, {
|
||||
fields: ['id', 'email', 'email_notifications', 'role.app_access'],
|
||||
});
|
||||
|
||||
const manageUserAccountUrl = new Url(env['PUBLIC_URL']).addPath('admin', 'users', user['id']).toString();
|
||||
|
||||
const html = data.message ? md(data.message) : '';
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('Integration Tests', () => {
|
||||
knex: db,
|
||||
schema: { collections: {}, relations: [] },
|
||||
});
|
||||
|
||||
helpers = getHelpers(db);
|
||||
});
|
||||
|
||||
@@ -219,6 +220,7 @@ describe('Integration Tests', () => {
|
||||
],
|
||||
'read'
|
||||
);
|
||||
|
||||
expect(result).toMatchObject([
|
||||
{
|
||||
[dateFieldId]: '2022-01-10',
|
||||
|
||||
@@ -58,6 +58,7 @@ export class PayloadService {
|
||||
public transformers: Transformers = {
|
||||
async hash({ action, value }) {
|
||||
if (!value) return;
|
||||
|
||||
if (action === 'create' || action === 'update') {
|
||||
return await generateHash(String(value));
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export class RelationsService {
|
||||
this.schemaInspector = options.knex ? createInspector(options.knex) : getSchemaInspector();
|
||||
this.schema = options.schema;
|
||||
this.accountability = options.accountability || null;
|
||||
|
||||
this.relationsItemService = new ItemsService('directus_relations', {
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
@@ -85,6 +86,7 @@ export class RelationsService {
|
||||
});
|
||||
|
||||
if (!permissions || !permissions.fields) throw new ForbiddenException();
|
||||
|
||||
if (permissions.fields.includes('*') === false) {
|
||||
const allowedFields = permissions.fields;
|
||||
if (allowedFields.includes(field) === false) throw new ForbiddenException();
|
||||
@@ -112,6 +114,7 @@ export class RelationsService {
|
||||
const schemaRow = (await this.schemaInspector.foreignKeys(collection)).find(
|
||||
(foreignKey) => foreignKey.column === field
|
||||
);
|
||||
|
||||
const stitched = this.stitchRelations(metaRow, schemaRow ? [schemaRow] : []);
|
||||
const results = await this.filterForbidden(stitched);
|
||||
|
||||
@@ -189,6 +192,7 @@ export class RelationsService {
|
||||
this.alterType(table, relation);
|
||||
|
||||
const constraintName: string = getDefaultIndexName('foreign', relation.collection!, relation.field!);
|
||||
|
||||
const builder = table
|
||||
.foreign(relation.field!, constraintName)
|
||||
.references(
|
||||
@@ -558,6 +562,7 @@ export class RelationsService {
|
||||
*/
|
||||
private alterType(table: Knex.TableBuilder, relation: Partial<Relation>) {
|
||||
const m2oFieldDBType = this.schema.collections[relation.collection!]!.fields[relation.field!]!.dbType;
|
||||
|
||||
const relatedFieldDBType =
|
||||
this.schema.collections[relation.related_collection!]!.fields[
|
||||
this.schema.collections[relation.related_collection!]!.primary
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('Integration Tests', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
tracker.on.any('directus_roles').response({});
|
||||
|
||||
tracker.on
|
||||
.select(/"directus_roles"."id" from "directus_roles" order by "directus_roles"."id" asc limit .*/)
|
||||
.response([]);
|
||||
@@ -73,6 +74,7 @@ describe('Integration Tests', () => {
|
||||
knex: db,
|
||||
schema: testSchema,
|
||||
});
|
||||
|
||||
superUpdateOne = vi.spyOn(ItemsService.prototype, 'updateOne');
|
||||
});
|
||||
|
||||
@@ -89,6 +91,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [userId1, userId2],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
|
||||
@@ -101,7 +104,9 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [userId1],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
|
||||
tracker.on
|
||||
.select('select "id" from "directus_users" where "role" = ?')
|
||||
.responseOnce([{ id: userId1 }, { id: userId2 }]);
|
||||
@@ -115,6 +120,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
@@ -134,6 +140,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 0 });
|
||||
@@ -150,9 +157,11 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
|
||||
expect(superUpdateOne).toHaveBeenCalled();
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException.message).toBe(
|
||||
`You can't remove the last admin user from the admin role.`
|
||||
);
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException).toBeInstanceOf(
|
||||
UnprocessableEntityException
|
||||
);
|
||||
@@ -164,6 +173,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [{ id: userId1 }, { id: userId2 }],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
|
||||
@@ -176,7 +186,9 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [{ id: userId1 }],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
|
||||
tracker.on
|
||||
.select('select "id" from "directus_users" where "role" = ?')
|
||||
.responseOnce([{ id: userId1 }, { id: userId2 }]);
|
||||
@@ -190,6 +202,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
@@ -209,6 +222,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 0 });
|
||||
@@ -225,9 +239,11 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
|
||||
expect(superUpdateOne).toHaveBeenCalled();
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException.message).toBe(
|
||||
`You can't remove the last admin user from the admin role.`
|
||||
);
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException).toBeInstanceOf(
|
||||
UnprocessableEntityException
|
||||
);
|
||||
@@ -243,6 +259,7 @@ describe('Integration Tests', () => {
|
||||
delete: [],
|
||||
},
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
|
||||
@@ -259,6 +276,7 @@ describe('Integration Tests', () => {
|
||||
delete: [],
|
||||
},
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
@@ -276,10 +294,13 @@ describe('Integration Tests', () => {
|
||||
delete: [userId2],
|
||||
},
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
|
||||
tracker.on
|
||||
.select('select "id" from "directus_users" where "role" = ?')
|
||||
.responseOnce([{ id: userId1 }, { id: userId2 }]);
|
||||
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
|
||||
const result = await service.updateOne(adminRoleId, data);
|
||||
@@ -295,6 +316,7 @@ describe('Integration Tests', () => {
|
||||
delete: [userId1],
|
||||
},
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
@@ -318,6 +340,7 @@ describe('Integration Tests', () => {
|
||||
delete: [userId1],
|
||||
},
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 0 });
|
||||
@@ -334,9 +357,11 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
|
||||
expect(superUpdateOne).toHaveBeenCalled();
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException.message).toBe(
|
||||
`You can't remove the last admin user from the admin role.`
|
||||
);
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException).toBeInstanceOf(
|
||||
UnprocessableEntityException
|
||||
);
|
||||
@@ -352,6 +377,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [userId1, userId2],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
@@ -371,6 +397,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [userId1, userId2],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 0 });
|
||||
@@ -387,9 +414,11 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
|
||||
expect(superUpdateOne).toHaveBeenCalled();
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException.message).toBe(
|
||||
`You can't remove the last admin user from the admin role.`
|
||||
);
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException).toBeInstanceOf(
|
||||
UnprocessableEntityException
|
||||
);
|
||||
@@ -399,10 +428,13 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [userId1],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
|
||||
tracker.on
|
||||
.select('select "id" from "directus_users" where "role" = ?')
|
||||
.responseOnce([{ id: userId1 }, { id: userId2 }]);
|
||||
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
|
||||
const result = await service.updateOne(adminRoleId, data);
|
||||
@@ -414,6 +446,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
@@ -433,6 +466,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 0 });
|
||||
@@ -449,9 +483,11 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
|
||||
expect(superUpdateOne).toHaveBeenCalled();
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException.message).toBe(
|
||||
`You can't remove the last admin user from the admin role.`
|
||||
);
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException).toBeInstanceOf(
|
||||
UnprocessableEntityException
|
||||
);
|
||||
@@ -463,6 +499,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [{ id: userId1 }, { id: userId2 }],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
@@ -482,6 +519,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [{ id: userId1 }, { id: userId2 }],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 0 });
|
||||
@@ -498,9 +536,11 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
|
||||
expect(superUpdateOne).toHaveBeenCalled();
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException.message).toBe(
|
||||
`You can't remove the last admin user from the admin role.`
|
||||
);
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException).toBeInstanceOf(
|
||||
UnprocessableEntityException
|
||||
);
|
||||
@@ -510,10 +550,13 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [{ id: userId1 }],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
|
||||
tracker.on
|
||||
.select('select "id" from "directus_users" where "role" = ?')
|
||||
.responseOnce([{ id: userId1 }, { id: userId2 }]);
|
||||
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
|
||||
const result = await service.updateOne(adminRoleId, data);
|
||||
@@ -525,6 +568,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
@@ -544,6 +588,7 @@ describe('Integration Tests', () => {
|
||||
const data: Record<string, any> = {
|
||||
users: [],
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 0 });
|
||||
@@ -560,9 +605,11 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
|
||||
expect(superUpdateOne).toHaveBeenCalled();
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException.message).toBe(
|
||||
`You can't remove the last admin user from the admin role.`
|
||||
);
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException).toBeInstanceOf(
|
||||
UnprocessableEntityException
|
||||
);
|
||||
@@ -578,6 +625,7 @@ describe('Integration Tests', () => {
|
||||
delete: [],
|
||||
},
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
@@ -595,6 +643,7 @@ describe('Integration Tests', () => {
|
||||
delete: [],
|
||||
},
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
@@ -618,6 +667,7 @@ describe('Integration Tests', () => {
|
||||
delete: [],
|
||||
},
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 0 });
|
||||
@@ -634,9 +684,11 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
|
||||
expect(superUpdateOne).toHaveBeenCalled();
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException.message).toBe(
|
||||
`You can't remove the last admin user from the admin role.`
|
||||
);
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException).toBeInstanceOf(
|
||||
UnprocessableEntityException
|
||||
);
|
||||
@@ -650,10 +702,13 @@ describe('Integration Tests', () => {
|
||||
delete: [userId2],
|
||||
},
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
|
||||
tracker.on
|
||||
.select('select "id" from "directus_users" where "role" = ?')
|
||||
.responseOnce([{ id: userId1 }, { id: userId2 }]);
|
||||
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
|
||||
const result = await service.updateOne(adminRoleId, data);
|
||||
@@ -669,6 +724,7 @@ describe('Integration Tests', () => {
|
||||
delete: [userId1],
|
||||
},
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 1 });
|
||||
@@ -692,6 +748,7 @@ describe('Integration Tests', () => {
|
||||
delete: [userId1],
|
||||
},
|
||||
};
|
||||
|
||||
tracker.on.select('select "admin_access" from "directus_roles"').responseOnce({ admin_access });
|
||||
tracker.on.select('select "id" from "directus_users" where "role" = ?').responseOnce([{ id: userId1 }]);
|
||||
tracker.on.select('select count(*) as "count" from "directus_users"').responseOnce({ count: 0 });
|
||||
@@ -708,9 +765,11 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
|
||||
expect(superUpdateOne).toHaveBeenCalled();
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException.message).toBe(
|
||||
`You can't remove the last admin user from the admin role.`
|
||||
);
|
||||
|
||||
expect(superUpdateOne.mock.lastCall![2].preMutationException).toBeInstanceOf(
|
||||
UnprocessableEntityException
|
||||
);
|
||||
@@ -769,6 +828,7 @@ describe('Integration Tests', () => {
|
||||
checkForOtherAdminRolesSpy = vi
|
||||
.spyOn(RolesService.prototype as any, 'checkForOtherAdminRoles')
|
||||
.mockResolvedValueOnce(true);
|
||||
|
||||
checkForOtherAdminUsersSpy = vi
|
||||
.spyOn(RolesService.prototype as any, 'checkForOtherAdminUsers')
|
||||
.mockResolvedValueOnce(true);
|
||||
@@ -829,6 +889,7 @@ describe('Integration Tests', () => {
|
||||
await service.updateBatch([{ id: 1 }]);
|
||||
expect(checkForOtherAdminRolesSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should checkForOtherAdminRoles once', async () => {
|
||||
await service.updateBatch([{ id: 1, admin_access: false }]);
|
||||
expect(checkForOtherAdminRolesSpy).toBeCalledTimes(1);
|
||||
|
||||
@@ -41,6 +41,7 @@ export class RolesService extends ItemsService {
|
||||
const usersThatWereInRoleBefore = (await this.knex.select('id').from('directus_users').where('role', '=', key)).map(
|
||||
(user) => user.id
|
||||
);
|
||||
|
||||
const usersThatAreRemoved = usersThatWereInRoleBefore.filter((id) =>
|
||||
Array.isArray(users) ? userKeys.includes(id) === false : users.delete.includes(id) === true
|
||||
);
|
||||
|
||||
@@ -148,6 +148,7 @@ export class ServerService {
|
||||
logger.warn(
|
||||
`${service} in WARN state, the observed value ${healthCheck.observedValue} is above the threshold of ${healthCheck.threshold}${healthCheck.observedUnit}`
|
||||
);
|
||||
|
||||
data.status = 'warn';
|
||||
continue;
|
||||
}
|
||||
@@ -358,6 +359,7 @@ export class ServerService {
|
||||
for (const location of toArray(env['STORAGE_LOCATIONS'])) {
|
||||
const disk = storage.location(location);
|
||||
const envThresholdKey = `STORAGE_${location}_HEALTHCHECK_THRESHOLD`.toUpperCase();
|
||||
|
||||
checks[`storage:${location}:responseTime`] = [
|
||||
{
|
||||
status: 'ok',
|
||||
@@ -373,6 +375,7 @@ export class ServerService {
|
||||
try {
|
||||
await disk.write(`health-${checkID}`, Readable.from(['check']));
|
||||
const fileStream = await disk.read(`health-${checkID}`);
|
||||
|
||||
fileStream.on('data', async () => {
|
||||
fileStream.destroy();
|
||||
await disk.delete(`health-${checkID}`);
|
||||
|
||||
@@ -150,9 +150,11 @@ describe('Integration Tests', () => {
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
vi.spyOn(RelationsService.prototype, 'readAll').mockResolvedValue([]);
|
||||
|
||||
const spec = await service.oas.generate();
|
||||
|
||||
expect(spec.components?.schemas).toMatchInlineSnapshot(`
|
||||
{
|
||||
"Diff": {
|
||||
@@ -421,6 +423,7 @@ describe('Integration Tests', () => {
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
vi.spyOn(RelationsService.prototype, 'readAll').mockResolvedValue([]);
|
||||
|
||||
const spec = await service.oas.generate();
|
||||
|
||||
@@ -439,6 +439,7 @@ class OASSpecsService implements SpecificationSubService {
|
||||
|
||||
if (relationType === 'm2o') {
|
||||
const relatedTag = tags.find((tag) => tag['x-collection'] === relation.related_collection);
|
||||
|
||||
const relatedPrimaryKeyField = fields.find(
|
||||
(field) => field.collection === relation.related_collection && field.schema?.is_primary_key
|
||||
);
|
||||
@@ -455,6 +456,7 @@ class OASSpecsService implements SpecificationSubService {
|
||||
];
|
||||
} else if (relationType === 'o2m') {
|
||||
const relatedTag = tags.find((tag) => tag['x-collection'] === relation.collection);
|
||||
|
||||
const relatedPrimaryKeyField = fields.find(
|
||||
(field) => field.collection === relation.collection && field.schema?.is_primary_key
|
||||
);
|
||||
@@ -462,6 +464,7 @@ class OASSpecsService implements SpecificationSubService {
|
||||
if (!relatedTag || !relatedPrimaryKeyField) return propertyObject;
|
||||
|
||||
propertyObject.type = 'array';
|
||||
|
||||
propertyObject.items = {
|
||||
oneOf: [
|
||||
{
|
||||
@@ -476,6 +479,7 @@ class OASSpecsService implements SpecificationSubService {
|
||||
const relatedTags = tags.filter((tag) => relation.meta!.one_allowed_collections!.includes(tag['x-collection']));
|
||||
|
||||
propertyObject.type = 'array';
|
||||
|
||||
propertyObject.items = {
|
||||
oneOf: [
|
||||
{
|
||||
|
||||
@@ -122,12 +122,15 @@ describe('Integration Tests', () => {
|
||||
checkUniqueEmailsSpy = vi
|
||||
.spyOn(UsersService.prototype as any, 'checkUniqueEmails')
|
||||
.mockImplementation(() => vi.fn());
|
||||
|
||||
checkPasswordPolicySpy = vi
|
||||
.spyOn(UsersService.prototype as any, 'checkPasswordPolicy')
|
||||
.mockResolvedValue(() => vi.fn());
|
||||
|
||||
checkRemainingAdminExistenceSpy = vi
|
||||
.spyOn(UsersService.prototype as any, 'checkRemainingAdminExistence')
|
||||
.mockResolvedValue(() => vi.fn());
|
||||
|
||||
checkRemainingActiveAdminSpy = vi
|
||||
.spyOn(UsersService.prototype as any, 'checkRemainingActiveAdmin')
|
||||
.mockResolvedValue(() => vi.fn());
|
||||
@@ -255,9 +258,11 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
|
||||
expect(superUpdateManySpy).toHaveBeenCalled();
|
||||
|
||||
expect(superUpdateManySpy.mock.lastCall![2].preMutationException.message).toBe(
|
||||
`You can't change the "${field}" value manually.`
|
||||
);
|
||||
|
||||
expect(superUpdateManySpy.mock.lastCall![2].preMutationException).toBeInstanceOf(InvalidPayloadException);
|
||||
}
|
||||
);
|
||||
@@ -370,9 +375,11 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
|
||||
expect(superUpdateManySpy).toHaveBeenCalled();
|
||||
|
||||
expect(superUpdateManySpy.mock.lastCall![2].preMutationException.message).toBe(
|
||||
`You can't change the "${field}" value manually.`
|
||||
);
|
||||
|
||||
expect(superUpdateManySpy.mock.lastCall![2].preMutationException).toBeInstanceOf(InvalidPayloadException);
|
||||
}
|
||||
);
|
||||
@@ -505,9 +512,11 @@ describe('Integration Tests', () => {
|
||||
}
|
||||
|
||||
expect(superUpdateManySpy).toHaveBeenCalled();
|
||||
|
||||
expect(superUpdateManySpy.mock.lastCall![2].preMutationException.message).toBe(
|
||||
`You can't change the "${field}" value manually.`
|
||||
);
|
||||
|
||||
expect(superUpdateManySpy.mock.lastCall![2].preMutationException).toBeInstanceOf(InvalidPayloadException);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -345,6 +345,7 @@ export class UsersService extends ItemsService {
|
||||
}
|
||||
|
||||
const emails = toArray(email);
|
||||
|
||||
const mailService = new MailService({
|
||||
schema: this.schema,
|
||||
accountability: this.accountability,
|
||||
@@ -428,9 +429,11 @@ export class UsersService extends ItemsService {
|
||||
|
||||
const payload = { email, scope: 'password-reset', hash: getSimpleHash('' + user.password) };
|
||||
const token = jwt.sign(payload, env['SECRET'] as string, { expiresIn: '1d', issuer: 'directus' });
|
||||
|
||||
const acceptURL = url
|
||||
? new Url(url).setQuery('token', token).toString()
|
||||
: new Url(env['PUBLIC_URL']).addPath('admin', 'reset-password').setQuery('token', token).toString();
|
||||
|
||||
const subjectLine = subject ? subject : 'Password Reset Request';
|
||||
|
||||
await mailService.send({
|
||||
|
||||
@@ -61,6 +61,7 @@ export class UtilsService {
|
||||
|
||||
for (const row of rowsWithoutSortValue) {
|
||||
lastSortValue++;
|
||||
|
||||
await this.knex(collection)
|
||||
.update({ [sortField]: lastSortValue })
|
||||
.where({ [primaryKeyField]: row[primaryKeyField] });
|
||||
@@ -93,6 +94,7 @@ export class UtilsService {
|
||||
.from(collection)
|
||||
.where({ [primaryKeyField]: to })
|
||||
.first();
|
||||
|
||||
const targetSortValue = targetSortValueResponse[sortField];
|
||||
|
||||
const sourceSortValueResponse = await this.knex
|
||||
@@ -100,6 +102,7 @@ export class UtilsService {
|
||||
.from(collection)
|
||||
.where({ [primaryKeyField]: item })
|
||||
.first();
|
||||
|
||||
const sourceSortValue = sourceSortValueResponse[sortField];
|
||||
|
||||
// Set the target item to the new sort value
|
||||
|
||||
@@ -67,6 +67,7 @@ describe('Integration Tests', () => {
|
||||
relations: [],
|
||||
},
|
||||
});
|
||||
|
||||
messengerPublishSpy = vi.spyOn(getMessenger(), 'publish');
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ vi.mock('../env');
|
||||
|
||||
let mockStorage: StorageManager;
|
||||
let mockDriver: typeof Driver;
|
||||
|
||||
let sample: {
|
||||
name: string;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ let sample: {
|
||||
};
|
||||
locations: string[];
|
||||
};
|
||||
|
||||
let mockStorage: StorageManager;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -42,9 +43,11 @@ beforeEach(() => {
|
||||
} as unknown as StorageManager;
|
||||
|
||||
vi.mocked(getConfigFromEnv).mockImplementation((name) => sample.options[name]!);
|
||||
|
||||
vi.mocked(getEnv).mockReturnValue({
|
||||
STORAGE_LOCATIONS: sample.locations.join(', '),
|
||||
});
|
||||
|
||||
vi.mocked(toArray).mockReturnValue(sample.locations);
|
||||
});
|
||||
|
||||
@@ -61,6 +64,7 @@ test('Gets config for each location', async () => {
|
||||
await registerLocations(mockStorage);
|
||||
|
||||
expect(getConfigFromEnv).toHaveBeenCalledTimes(sample.locations.length);
|
||||
|
||||
sample.locations.forEach((location) =>
|
||||
expect(getConfigFromEnv).toHaveBeenCalledWith(`STORAGE_${location.toUpperCase()}_`)
|
||||
);
|
||||
@@ -70,6 +74,7 @@ test('Registers location with driver options for each location', async () => {
|
||||
await registerLocations(mockStorage);
|
||||
|
||||
expect(mockStorage.registerLocation).toHaveBeenCalledTimes(sample.locations.length);
|
||||
|
||||
sample.locations.forEach((location) => {
|
||||
const { driver, ...options } = sample.options[`STORAGE_${location.toUpperCase()}_`]!;
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export async function applyDiff(
|
||||
const schema = options?.schema ?? (await getSchema({ database, bypassCache: true }));
|
||||
|
||||
const nestedActionEvents: ActionEventParams[] = [];
|
||||
|
||||
const mutationOptions: MutationOptions = {
|
||||
autoPurgeSystemCache: false,
|
||||
bypassEmitAction: (params) => nestedActionEvents.push(params),
|
||||
@@ -119,6 +120,7 @@ export async function applyDiff(
|
||||
logger.error(
|
||||
`Failed to delete collection "${collection}" due to relation "${relation.collection}.${relation.field}"`
|
||||
);
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -153,6 +155,7 @@ export async function applyDiff(
|
||||
|
||||
// Check if parent collection already exists in schema
|
||||
const parentExists = currentSnapshot.collections.find((c) => c.collection === groupName) !== undefined;
|
||||
|
||||
// If this is a new collection and the parent collection doesn't exist in current schema ->
|
||||
// Check if the parent collection will be created as part of applying this snapshot ->
|
||||
// If yes -> this collection will be created recursively
|
||||
@@ -165,6 +168,7 @@ export async function applyDiff(
|
||||
snapshotDiff.collections.filter(
|
||||
({ collection, diff }) => diff[0]?.kind === DiffKind.NEW && collection === groupName
|
||||
).length > 0;
|
||||
|
||||
// Has group, but parent is not new, parent is also not being created in this snapshot apply
|
||||
if (parentExists && !parentWillBeCreatedInThisApply) return true;
|
||||
|
||||
@@ -230,6 +234,7 @@ export async function applyDiff(
|
||||
deepDiff.applyChange(acc, undefined, currentDiff);
|
||||
return acc;
|
||||
}, cloneDeep(currentField));
|
||||
|
||||
await fieldsService.updateField(collection, newValues, mutationOptions);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to update field "${collection}.${field}"`);
|
||||
@@ -286,6 +291,7 @@ export async function applyDiff(
|
||||
deepDiff.applyChange(acc, undefined, currentDiff);
|
||||
return acc;
|
||||
}, cloneDeep(currentRelation));
|
||||
|
||||
await relationsService.updateOne(collection, field, newValues, mutationOptions);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to update relation "${collection}.${field}"`);
|
||||
|
||||
@@ -165,6 +165,7 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
|
||||
`${aliasedParentCollection}.${relation.field}`,
|
||||
`${alias}.${schema.collections[relation.related_collection!]!.primary}`
|
||||
);
|
||||
|
||||
aliasMap[aliasKey]!.collection = relation.related_collection!;
|
||||
} else if (relationType === 'a2o') {
|
||||
const pathScope = pathParts[0]!.split(':')[1];
|
||||
@@ -187,6 +188,7 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
aliasMap[aliasKey]!.collection = pathScope;
|
||||
} else if (relationType === 'o2a') {
|
||||
rootQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
|
||||
@@ -201,6 +203,7 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
aliasMap[aliasKey]!.collection = relation.collection;
|
||||
|
||||
hasMultiRelational = true;
|
||||
@@ -210,6 +213,7 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
|
||||
`${aliasedParentCollection}.${schema.collections[relation.related_collection!]!.primary}`,
|
||||
`${alias}.${relation.field}`
|
||||
);
|
||||
|
||||
aliasMap[aliasKey]!.collection = relation.collection;
|
||||
|
||||
hasMultiRelational = true;
|
||||
@@ -295,6 +299,7 @@ export function applySort(
|
||||
relations,
|
||||
schema,
|
||||
});
|
||||
|
||||
const [alias, field] = columnPath.split('.');
|
||||
|
||||
if (!hasMultiRelationalSort) {
|
||||
|
||||
@@ -50,6 +50,7 @@ describe('filter items', () => {
|
||||
{},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toStrictEqual(items.filter((item) => item.action === 'read'));
|
||||
});
|
||||
|
||||
@@ -59,6 +60,7 @@ describe('filter items', () => {
|
||||
_eq: 'read',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toStrictEqual(items.filter((item) => item.action === 'read'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,8 +3,10 @@ import { getConfigFromEnv } from './get-config-from-env.js';
|
||||
|
||||
export function generateHash(stringToHash: string): Promise<string> {
|
||||
const argon2HashConfigOptions = getConfigFromEnv('HASH_', 'HASH_RAW'); // Disallow the HASH_RAW option, see https://github.com/directus/directus/discussions/7670#discussioncomment-1255805
|
||||
|
||||
// associatedData, if specified, must be passed as a Buffer to argon2.hash, see https://github.com/ranisalt/node-argon2/wiki/Options#associateddata
|
||||
'associatedData' in argon2HashConfigOptions &&
|
||||
(argon2HashConfigOptions['associatedData'] = Buffer.from(argon2HashConfigOptions['associatedData']));
|
||||
|
||||
return argon2.hash(stringToHash, argon2HashConfigOptions);
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ export default async function getASTFromQuery(
|
||||
query: {},
|
||||
relatedCollection: foundRelation.collection,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,8 +204,10 @@ describe('get cache headers', () => {
|
||||
return matchingKey ? (scenario.input.headers as any)?.[matchingKey] : undefined;
|
||||
}),
|
||||
} as Partial<Request>;
|
||||
|
||||
factoryEnv = scenario.input.env;
|
||||
const { ttl, globalCacheSettings, personalized } = scenario.input;
|
||||
|
||||
expect(getCacheControlHeader(mockRequest as Request, ttl, globalCacheSettings, personalized)).toEqual(
|
||||
scenario.output
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ vi.mock('../../src/env', () => {
|
||||
CAMELCASE_OBJECT__FIRST_KEY: 'firstValue',
|
||||
CAMELCASE_OBJECT__SECOND_KEY: 'secondValue',
|
||||
};
|
||||
|
||||
return {
|
||||
default: MOCK_ENV,
|
||||
getEnv: () => MOCK_ENV,
|
||||
|
||||
@@ -30,6 +30,7 @@ export function getConfigFromEnv(
|
||||
const path = key
|
||||
.split('__')
|
||||
.map((key, index) => (index === 0 ? transform(transform(key.slice(prefix.length))) : transform(key)));
|
||||
|
||||
set(config, path.join('.'), value);
|
||||
} else {
|
||||
config[transform(key.slice(prefix.length))] = value;
|
||||
|
||||
@@ -10,6 +10,7 @@ const query = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = JSON.stringify({ id: 1 });
|
||||
const additionalProperty = 'test';
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ export default function getLocalType(
|
||||
if (special.includes('uuid') || special.includes('file')) return 'uuid';
|
||||
if (special.includes('cast-timestamp')) return 'timestamp';
|
||||
if (special.includes('cast-datetime')) return 'dateTime';
|
||||
|
||||
if (type?.startsWith('geometry')) {
|
||||
return (special[0] as Type) || 'geometry';
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ test('Returns the payload of an access token', () => {
|
||||
const payload = { id: 1, role: 1, app_access: true, admin_access: true };
|
||||
const token = jwt.sign(payload, secret, options);
|
||||
const result = verifyAccessJWT(token, secret);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 1,
|
||||
role: 1,
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('merging permissions', () => {
|
||||
{ ...permissionTemplate, permissions: conditionalFilter },
|
||||
{ ...permissionTemplate, permissions: conditionalFilter2 }
|
||||
);
|
||||
|
||||
expect(mergedPermission).toStrictEqual({
|
||||
...permissionTemplate,
|
||||
permissions: {
|
||||
@@ -36,6 +37,7 @@ describe('merging permissions', () => {
|
||||
{ ...permissionTemplate, validation: conditionalFilter },
|
||||
{ ...permissionTemplate, validation: conditionalFilter2 }
|
||||
);
|
||||
|
||||
expect(mergedPermission).toStrictEqual({
|
||||
...permissionTemplate,
|
||||
validation: {
|
||||
@@ -50,6 +52,7 @@ describe('merging permissions', () => {
|
||||
{ ...permissionTemplate, permissions: conditionalFilter },
|
||||
{ ...permissionTemplate, permissions: conditionalFilter2 }
|
||||
);
|
||||
|
||||
expect(mergedPermission).toStrictEqual({
|
||||
...permissionTemplate,
|
||||
permissions: {
|
||||
@@ -64,6 +67,7 @@ describe('merging permissions', () => {
|
||||
{ ...permissionTemplate, validation: conditionalFilter },
|
||||
{ ...permissionTemplate, validation: conditionalFilter2 }
|
||||
);
|
||||
|
||||
expect(mergedPermission).toStrictEqual({
|
||||
...permissionTemplate,
|
||||
validation: {
|
||||
@@ -78,6 +82,7 @@ describe('merging permissions', () => {
|
||||
{ ...permissionTemplate, permissions: fullFilter },
|
||||
{ ...permissionTemplate, permissions: conditionalFilter }
|
||||
);
|
||||
|
||||
expect(mergedPermission).toStrictEqual({ ...permissionTemplate, permissions: fullFilter });
|
||||
});
|
||||
|
||||
@@ -87,6 +92,7 @@ describe('merging permissions', () => {
|
||||
{ ...permissionTemplate, validation: fullFilter },
|
||||
{ ...permissionTemplate, validation: conditionalFilter }
|
||||
);
|
||||
|
||||
expect(mergedPermission).toStrictEqual({ ...permissionTemplate, validation: fullFilter });
|
||||
});
|
||||
|
||||
@@ -96,6 +102,7 @@ describe('merging permissions', () => {
|
||||
{ ...permissionTemplate, permissions: fullFilter },
|
||||
{ ...permissionTemplate, permissions: conditionalFilter }
|
||||
);
|
||||
|
||||
const expectedPermission = {
|
||||
...permissionTemplate,
|
||||
permissions: {
|
||||
@@ -112,6 +119,7 @@ describe('merging permissions', () => {
|
||||
{ ...permissionTemplate, validation: fullFilter },
|
||||
{ ...permissionTemplate, validation: conditionalFilter }
|
||||
);
|
||||
|
||||
const expectedPermission = {
|
||||
...permissionTemplate,
|
||||
validation: {
|
||||
|
||||
@@ -27,6 +27,7 @@ export function sanitizeField(field: Field | undefined, sanitizeAllSchema = fals
|
||||
if (!field) return field;
|
||||
|
||||
const defaultPaths = ['collection', 'field', 'type', 'meta', 'name', 'children'];
|
||||
|
||||
const pickedPaths = sanitizeAllSchema
|
||||
? defaultPaths
|
||||
: [
|
||||
|
||||
@@ -19,6 +19,7 @@ export class Url {
|
||||
!isProtocolRelative && !isRootRelative && !isPathRelative
|
||||
? parsedUrl.protocol.substring(0, parsedUrl.protocol.length - 1)
|
||||
: null;
|
||||
|
||||
this.host = !isRootRelative && !isPathRelative ? parsedUrl.hostname : null;
|
||||
this.port = parsedUrl.port !== '' ? parsedUrl.port : null;
|
||||
this.path = parsedUrl.pathname.split('/').filter((p) => p !== '');
|
||||
|
||||
@@ -22,6 +22,7 @@ test('should fail on invalid hash', () => {
|
||||
hash: 'abc',
|
||||
diff: { collections: [{ collection: 'test', diff: [] }], fields: [], relations: [] },
|
||||
} as SnapshotDiffWithHash;
|
||||
|
||||
const snapshot = { hash: 'xyz' } as SnapshotWithHash;
|
||||
|
||||
expect(() => validateApplyDiff(diff, snapshot)).toThrowError(
|
||||
@@ -56,6 +57,7 @@ describe('should throw accurate error', () => {
|
||||
const diff = baseDiff({
|
||||
collections: [{ collection: 'test', diff: [{ kind: 'N', rhs: {} as Collection }] }],
|
||||
});
|
||||
|
||||
const snapshot = baseSnapshot({ collections: [{ collection: 'test' } as Collection] });
|
||||
|
||||
expect(() => validateApplyDiff(diff, snapshot)).toThrowError(
|
||||
@@ -77,6 +79,7 @@ describe('should throw accurate error', () => {
|
||||
const diff = baseDiff({
|
||||
fields: [{ collection: 'test', field: 'test', diff: [{ kind: 'N', rhs: {} as SnapshotField }] }],
|
||||
});
|
||||
|
||||
const snapshot = baseSnapshot({ fields: [{ collection: 'test', field: 'test' } as SnapshotField] });
|
||||
|
||||
expect(() => validateApplyDiff(diff, snapshot)).toThrowError(
|
||||
@@ -105,6 +108,7 @@ describe('should throw accurate error', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const snapshot = baseSnapshot({
|
||||
relations: [{ collection: 'test', field: 'test', related_collection: 'relation' } as SnapshotRelation],
|
||||
});
|
||||
@@ -248,6 +252,7 @@ test('should not throw error for diffs with varying types of lhs/rhs', () => {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const snapshot = { hash: 'abc' } as SnapshotWithHash;
|
||||
|
||||
expect(() => validateApplyDiff(diff, snapshot)).not.toThrow();
|
||||
@@ -289,6 +294,7 @@ test('should not throw error for relation diff with null related_collection (app
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const snapshot = { hash: 'abc' } as SnapshotWithHash;
|
||||
|
||||
expect(() => validateApplyDiff(diff, snapshot)).not.toThrow();
|
||||
@@ -299,6 +305,7 @@ test('should detect empty diff', () => {
|
||||
hash: 'abc',
|
||||
diff: { collections: [], fields: [], relations: [] },
|
||||
};
|
||||
|
||||
const snapshot = {} as SnapshotWithHash;
|
||||
|
||||
expect(validateApplyDiff(diff, snapshot)).toBe(false);
|
||||
@@ -309,6 +316,7 @@ test('should pass on valid diff', () => {
|
||||
hash: 'abc',
|
||||
diff: { collections: [{ collection: 'test', diff: [] }], fields: [], relations: [] },
|
||||
};
|
||||
|
||||
const snapshot = { hash: 'abc' } as SnapshotWithHash;
|
||||
|
||||
expect(validateApplyDiff(diff, snapshot)).toBe(true);
|
||||
|
||||
@@ -8,6 +8,7 @@ vi.mock('../env', () => ({
|
||||
PRESENT_TEST_VARIABLE: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../logger', () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
|
||||
@@ -92,6 +92,7 @@ describe('validate keys', () => {
|
||||
expect(() =>
|
||||
validateKeys(schema, 'pk_uuid', 'id', [uuid(), 'fakeuuid-62d9-434d-a7c7-878c8376782e', uuid()])
|
||||
).toThrowError();
|
||||
|
||||
expect(() => validateKeys(schema, 'pk_uuid', 'id', [uuid(), 'invalid', uuid()])).toThrowError();
|
||||
expect(() => validateKeys(schema, 'pk_uuid', 'id', [uuid(), NaN, uuid()])).toThrowError();
|
||||
expect(() => validateKeys(schema, 'pk_uuid', 'id', [uuid(), 111, uuid()])).toThrowError();
|
||||
|
||||
@@ -3,10 +3,12 @@ import { validateQuery } from './validate-query.js';
|
||||
|
||||
vi.mock('../env', async () => {
|
||||
const actual = (await vi.importActual('../env')) as { default: Record<string, any> };
|
||||
|
||||
const MOCK_ENV = {
|
||||
...actual.default,
|
||||
MAX_QUERY_LIMIT: 100,
|
||||
};
|
||||
|
||||
return {
|
||||
default: MOCK_ENV,
|
||||
getEnv: () => MOCK_ENV,
|
||||
|
||||
@@ -134,6 +134,7 @@ function validateList(value: any, key: string) {
|
||||
|
||||
function validateBoolean(value: any, key: string) {
|
||||
if (value === null) return true;
|
||||
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new InvalidQueryException(`"${key}" has to be a boolean`);
|
||||
}
|
||||
@@ -143,6 +144,7 @@ function validateBoolean(value: any, key: string) {
|
||||
|
||||
function validateGeometry(value: any, key: string) {
|
||||
if (value === null) return true;
|
||||
|
||||
try {
|
||||
stringify(value);
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user