Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions spec/MongoStorageAdapter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -649,4 +649,179 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
});
});
}

describe('index creation options', () => {
beforeEach(async () => {
await new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses();
});

async function getIndexes(collectionName) {
const adapter = Config.get(Parse.applicationId).database.adapter;
const collections = await adapter.database.listCollections({ name: collectionName }).toArray();
if (collections.length === 0) {
return [];
}
return await adapter.database.collection(collectionName).indexes();
}

it('should skip username index when createIndexUserUsername is false', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexUserUsername: false },
});
const indexes = await getIndexes('_User');
expect(indexes.find(idx => idx.name === 'username_1')).toBeUndefined();
});

it('should create username index when createIndexUserUsername is true', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexUserUsername: true },
});
const indexes = await getIndexes('_User');
expect(indexes.find(idx => idx.name === 'username_1')).toBeDefined();
});

it('should skip case-insensitive username index when createIndexUserUsernameCaseInsensitive is false', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexUserUsernameCaseInsensitive: false },
});
const indexes = await getIndexes('_User');
expect(indexes.find(idx => idx.name === 'case_insensitive_username')).toBeUndefined();
});

it('should create case-insensitive username index when createIndexUserUsernameCaseInsensitive is true', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexUserUsernameCaseInsensitive: true },
});
const indexes = await getIndexes('_User');
expect(indexes.find(idx => idx.name === 'case_insensitive_username')).toBeDefined();
});

it('should skip email index when createIndexUserEmail is false', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexUserEmail: false },
});
const indexes = await getIndexes('_User');
expect(indexes.find(idx => idx.name === 'email_1')).toBeUndefined();
});

it('should create email index when createIndexUserEmail is true', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexUserEmail: true },
});
const indexes = await getIndexes('_User');
expect(indexes.find(idx => idx.name === 'email_1')).toBeDefined();
});

it('should skip case-insensitive email index when createIndexUserEmailCaseInsensitive is false', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexUserEmailCaseInsensitive: false },
});
const indexes = await getIndexes('_User');
expect(indexes.find(idx => idx.name === 'case_insensitive_email')).toBeUndefined();
});

it('should create case-insensitive email index when createIndexUserEmailCaseInsensitive is true', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexUserEmailCaseInsensitive: true },
});
const indexes = await getIndexes('_User');
expect(indexes.find(idx => idx.name === 'case_insensitive_email')).toBeDefined();
});

it('should skip email verify token index when createIndexUserEmailVerifyToken is false', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexUserEmailVerifyToken: false },
});
const indexes = await getIndexes('_User');
expect(indexes.find(idx => idx.name === '_email_verify_token' || idx.name === '_email_verify_token_1')).toBeUndefined();
});

it('should create email verify token index when createIndexUserEmailVerifyToken is true', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexUserEmailVerifyToken: true },
});
const indexes = await getIndexes('_User');
expect(indexes.find(idx => idx.name === '_email_verify_token' || idx.name === '_email_verify_token_1')).toBeDefined();
});

it('should skip password reset token index when createIndexUserPasswordResetToken is false', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexUserPasswordResetToken: false },
});
const indexes = await getIndexes('_User');
expect(indexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeUndefined();
});

it('should create password reset token index when createIndexUserPasswordResetToken is true', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexUserPasswordResetToken: true },
});
const indexes = await getIndexes('_User');
expect(indexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeDefined();
});

it('should skip role name index when createIndexRoleName is false', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexRoleName: false },
});
const indexes = await getIndexes('_Role');
expect(indexes.find(idx => idx.name === 'name_1')).toBeUndefined();
});

it('should create role name index when createIndexRoleName is true', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: { createIndexRoleName: true },
});
const indexes = await getIndexes('_Role');
expect(indexes.find(idx => idx.name === 'name_1')).toBeDefined();
});

it('should create all indexes by default when options are undefined', async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI,
databaseOptions: {},
});

const userIndexes = await getIndexes('_User');
const roleIndexes = await getIndexes('_Role');

// Verify all indexes are created with default behavior (backward compatibility)
expect(userIndexes.find(idx => idx.name === 'username_1')).toBeDefined();
expect(userIndexes.find(idx => idx.name === 'case_insensitive_username')).toBeDefined();
expect(userIndexes.find(idx => idx.name === 'email_1')).toBeDefined();
expect(userIndexes.find(idx => idx.name === 'case_insensitive_email')).toBeDefined();
expect(userIndexes.find(idx => idx.name === '_email_verify_token' || idx.name === '_email_verify_token_1')).toBeDefined();
expect(userIndexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeDefined();
expect(roleIndexes.find(idx => idx.name === 'name_1')).toBeDefined();
});
});
});
18 changes: 16 additions & 2 deletions src/Adapters/Storage/Mongo/MongoStorageAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,22 @@ export class MongoStorageAdapter implements StorageAdapter {
this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks;
this.schemaCacheTtl = mongoOptions.schemaCacheTtl;
this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation;
for (const key of ['enableSchemaHooks', 'schemaCacheTtl', 'maxTimeMS', 'disableIndexFieldValidation']) {
delete mongoOptions[key];
// Remove Parse Server-specific options that should not be passed to MongoDB client
// Note: We only delete from this._mongoOptions, not from the original mongoOptions object,
// because other components (like DatabaseController) need access to these options
for (const key of [
'enableSchemaHooks',
'schemaCacheTtl',
'maxTimeMS',
'disableIndexFieldValidation',
'createIndexUserUsername',
'createIndexUserUsernameCaseInsensitive',
'createIndexUserEmail',
'createIndexUserEmailCaseInsensitive',
'createIndexUserEmailVerifyToken',
'createIndexUserPasswordResetToken',
'createIndexRoleName',
]) {
delete this._mongoOptions[key];
}
}
Expand Down
74 changes: 45 additions & 29 deletions src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -1738,50 +1738,66 @@ class DatabaseController {
await this.loadSchema().then(schema => schema.enforceClassExists('_Role'));
await this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency'));

await this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']).catch(error => {
logger.warn('Unable to ensure uniqueness for usernames: ', error);
throw error;
});
const databaseOptions = this.options.databaseOptions || {};

if (databaseOptions.createIndexUserUsername !== false) {
await this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']).catch(error => {
logger.warn('Unable to ensure uniqueness for usernames: ', error);
throw error;
});
}

if (!this.options.enableCollationCaseComparison) {
if (databaseOptions.createIndexUserUsernameCaseInsensitive !== false) {
await this.adapter
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
.catch(error => {
logger.warn('Unable to create case insensitive username index: ', error);
throw error;
});
}

if (databaseOptions.createIndexUserEmailCaseInsensitive !== false) {
await this.adapter
.ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true)
.catch(error => {
logger.warn('Unable to create case insensitive email index: ', error);
throw error;
});
}
}

if (databaseOptions.createIndexUserEmail !== false) {
await this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']).catch(error => {
logger.warn('Unable to ensure uniqueness for user email addresses: ', error);
throw error;
});
}

if (databaseOptions.createIndexUserEmailVerifyToken !== false) {
await this.adapter
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
.ensureIndex('_User', requiredUserFields, ['_email_verify_token'], '_email_verify_token', false)
.catch(error => {
logger.warn('Unable to create case insensitive username index: ', error);
logger.warn('Unable to create index for email verification token: ', error);
throw error;
});
}

if (databaseOptions.createIndexUserPasswordResetToken !== false) {
await this.adapter
.ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true)
.ensureIndex('_User', requiredUserFields, ['_perishable_token'], '_perishable_token', false)
.catch(error => {
logger.warn('Unable to create case insensitive email index: ', error);
logger.warn('Unable to create index for password reset token: ', error);
throw error;
});
}

await this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']).catch(error => {
logger.warn('Unable to ensure uniqueness for user email addresses: ', error);
throw error;
});

await this.adapter
.ensureIndex('_User', requiredUserFields, ['_email_verify_token'], '_email_verify_token', false)
.catch(error => {
logger.warn('Unable to create index for email verification token: ', error);
throw error;
});

await this.adapter
.ensureIndex('_User', requiredUserFields, ['_perishable_token'], '_perishable_token', false)
.catch(error => {
logger.warn('Unable to create index for password reset token: ', error);
if (databaseOptions.createIndexRoleName !== false) {
await this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']).catch(error => {
logger.warn('Unable to ensure uniqueness for role name: ', error);
throw error;
});

await this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']).catch(error => {
logger.warn('Unable to ensure uniqueness for role name: ', error);
throw error;
});
}

await this.adapter
.ensureUniqueness('_Idempotency', requiredIdempotencyFields, ['reqId'])
Expand Down
49 changes: 49 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,55 @@ module.exports.DatabaseOptions = {
'The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout.',
action: parsers.numberParser('connectTimeoutMS'),
},
createIndexRoleName: {
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_ROLE_NAME',
help:
'Set to `true` to automatically create a unique index on the name field of the _Role collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.',
action: parsers.booleanParser,
default: true,
},
createIndexUserEmail: {
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL',
help:
'Set to `true` to automatically create indexes on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.',
action: parsers.booleanParser,
default: true,
},
createIndexUserEmailCaseInsensitive: {
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL_CASE_INSENSITIVE',
help:
'Set to `true` to automatically create a case-insensitive index on the email field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.',
action: parsers.booleanParser,
default: true,
},
createIndexUserEmailVerifyToken: {
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL_VERIFY_TOKEN',
help:
'Set to `true` to automatically create an index on the _email_verify_token field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.',
action: parsers.booleanParser,
default: true,
},
createIndexUserPasswordResetToken: {
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_PASSWORD_RESET_TOKEN',
help:
'Set to `true` to automatically create an index on the _perishable_token field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.',
action: parsers.booleanParser,
default: true,
},
createIndexUserUsername: {
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_USERNAME',
help:
'Set to `true` to automatically create indexes on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.',
action: parsers.booleanParser,
default: true,
},
createIndexUserUsernameCaseInsensitive: {
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_USERNAME_CASE_INSENSITIVE',
help:
'Set to `true` to automatically create a case-insensitive index on the username field of the _User collection on server start. Set to `false` to skip index creation. Default is `true`.<br><br>\u26A0\uFE0F When setting this option to `false` to manually create the index, keep in mind that the otherwise automatically created index may change in the future to be optimized for the internal usage by Parse Server.',
action: parsers.booleanParser,
default: true,
},
disableIndexFieldValidation: {
env: 'PARSE_SERVER_DATABASE_DISABLE_INDEX_FIELD_VALIDATION',
help:
Expand Down
Loading
Loading