Skip to content

Commit ea91aca

Browse files
authored
feat: Add options to skip automatic creation of internal database indexes on server start (#9897)
1 parent 4f4580a commit ea91aca

File tree

6 files changed

+313
-31
lines changed

6 files changed

+313
-31
lines changed

spec/MongoStorageAdapter.spec.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,4 +649,179 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
649649
});
650650
});
651651
}
652+
653+
describe('index creation options', () => {
654+
beforeEach(async () => {
655+
await new MongoStorageAdapter({ uri: databaseURI }).deleteAllClasses();
656+
});
657+
658+
async function getIndexes(collectionName) {
659+
const adapter = Config.get(Parse.applicationId).database.adapter;
660+
const collections = await adapter.database.listCollections({ name: collectionName }).toArray();
661+
if (collections.length === 0) {
662+
return [];
663+
}
664+
return await adapter.database.collection(collectionName).indexes();
665+
}
666+
667+
it('should skip username index when createIndexUserUsername is false', async () => {
668+
await reconfigureServer({
669+
databaseAdapter: undefined,
670+
databaseURI,
671+
databaseOptions: { createIndexUserUsername: false },
672+
});
673+
const indexes = await getIndexes('_User');
674+
expect(indexes.find(idx => idx.name === 'username_1')).toBeUndefined();
675+
});
676+
677+
it('should create username index when createIndexUserUsername is true', async () => {
678+
await reconfigureServer({
679+
databaseAdapter: undefined,
680+
databaseURI,
681+
databaseOptions: { createIndexUserUsername: true },
682+
});
683+
const indexes = await getIndexes('_User');
684+
expect(indexes.find(idx => idx.name === 'username_1')).toBeDefined();
685+
});
686+
687+
it('should skip case-insensitive username index when createIndexUserUsernameCaseInsensitive is false', async () => {
688+
await reconfigureServer({
689+
databaseAdapter: undefined,
690+
databaseURI,
691+
databaseOptions: { createIndexUserUsernameCaseInsensitive: false },
692+
});
693+
const indexes = await getIndexes('_User');
694+
expect(indexes.find(idx => idx.name === 'case_insensitive_username')).toBeUndefined();
695+
});
696+
697+
it('should create case-insensitive username index when createIndexUserUsernameCaseInsensitive is true', async () => {
698+
await reconfigureServer({
699+
databaseAdapter: undefined,
700+
databaseURI,
701+
databaseOptions: { createIndexUserUsernameCaseInsensitive: true },
702+
});
703+
const indexes = await getIndexes('_User');
704+
expect(indexes.find(idx => idx.name === 'case_insensitive_username')).toBeDefined();
705+
});
706+
707+
it('should skip email index when createIndexUserEmail is false', async () => {
708+
await reconfigureServer({
709+
databaseAdapter: undefined,
710+
databaseURI,
711+
databaseOptions: { createIndexUserEmail: false },
712+
});
713+
const indexes = await getIndexes('_User');
714+
expect(indexes.find(idx => idx.name === 'email_1')).toBeUndefined();
715+
});
716+
717+
it('should create email index when createIndexUserEmail is true', async () => {
718+
await reconfigureServer({
719+
databaseAdapter: undefined,
720+
databaseURI,
721+
databaseOptions: { createIndexUserEmail: true },
722+
});
723+
const indexes = await getIndexes('_User');
724+
expect(indexes.find(idx => idx.name === 'email_1')).toBeDefined();
725+
});
726+
727+
it('should skip case-insensitive email index when createIndexUserEmailCaseInsensitive is false', async () => {
728+
await reconfigureServer({
729+
databaseAdapter: undefined,
730+
databaseURI,
731+
databaseOptions: { createIndexUserEmailCaseInsensitive: false },
732+
});
733+
const indexes = await getIndexes('_User');
734+
expect(indexes.find(idx => idx.name === 'case_insensitive_email')).toBeUndefined();
735+
});
736+
737+
it('should create case-insensitive email index when createIndexUserEmailCaseInsensitive is true', async () => {
738+
await reconfigureServer({
739+
databaseAdapter: undefined,
740+
databaseURI,
741+
databaseOptions: { createIndexUserEmailCaseInsensitive: true },
742+
});
743+
const indexes = await getIndexes('_User');
744+
expect(indexes.find(idx => idx.name === 'case_insensitive_email')).toBeDefined();
745+
});
746+
747+
it('should skip email verify token index when createIndexUserEmailVerifyToken is false', async () => {
748+
await reconfigureServer({
749+
databaseAdapter: undefined,
750+
databaseURI,
751+
databaseOptions: { createIndexUserEmailVerifyToken: false },
752+
});
753+
const indexes = await getIndexes('_User');
754+
expect(indexes.find(idx => idx.name === '_email_verify_token' || idx.name === '_email_verify_token_1')).toBeUndefined();
755+
});
756+
757+
it('should create email verify token index when createIndexUserEmailVerifyToken is true', async () => {
758+
await reconfigureServer({
759+
databaseAdapter: undefined,
760+
databaseURI,
761+
databaseOptions: { createIndexUserEmailVerifyToken: true },
762+
});
763+
const indexes = await getIndexes('_User');
764+
expect(indexes.find(idx => idx.name === '_email_verify_token' || idx.name === '_email_verify_token_1')).toBeDefined();
765+
});
766+
767+
it('should skip password reset token index when createIndexUserPasswordResetToken is false', async () => {
768+
await reconfigureServer({
769+
databaseAdapter: undefined,
770+
databaseURI,
771+
databaseOptions: { createIndexUserPasswordResetToken: false },
772+
});
773+
const indexes = await getIndexes('_User');
774+
expect(indexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeUndefined();
775+
});
776+
777+
it('should create password reset token index when createIndexUserPasswordResetToken is true', async () => {
778+
await reconfigureServer({
779+
databaseAdapter: undefined,
780+
databaseURI,
781+
databaseOptions: { createIndexUserPasswordResetToken: true },
782+
});
783+
const indexes = await getIndexes('_User');
784+
expect(indexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeDefined();
785+
});
786+
787+
it('should skip role name index when createIndexRoleName is false', async () => {
788+
await reconfigureServer({
789+
databaseAdapter: undefined,
790+
databaseURI,
791+
databaseOptions: { createIndexRoleName: false },
792+
});
793+
const indexes = await getIndexes('_Role');
794+
expect(indexes.find(idx => idx.name === 'name_1')).toBeUndefined();
795+
});
796+
797+
it('should create role name index when createIndexRoleName is true', async () => {
798+
await reconfigureServer({
799+
databaseAdapter: undefined,
800+
databaseURI,
801+
databaseOptions: { createIndexRoleName: true },
802+
});
803+
const indexes = await getIndexes('_Role');
804+
expect(indexes.find(idx => idx.name === 'name_1')).toBeDefined();
805+
});
806+
807+
it('should create all indexes by default when options are undefined', async () => {
808+
await reconfigureServer({
809+
databaseAdapter: undefined,
810+
databaseURI,
811+
databaseOptions: {},
812+
});
813+
814+
const userIndexes = await getIndexes('_User');
815+
const roleIndexes = await getIndexes('_Role');
816+
817+
// Verify all indexes are created with default behavior (backward compatibility)
818+
expect(userIndexes.find(idx => idx.name === 'username_1')).toBeDefined();
819+
expect(userIndexes.find(idx => idx.name === 'case_insensitive_username')).toBeDefined();
820+
expect(userIndexes.find(idx => idx.name === 'email_1')).toBeDefined();
821+
expect(userIndexes.find(idx => idx.name === 'case_insensitive_email')).toBeDefined();
822+
expect(userIndexes.find(idx => idx.name === '_email_verify_token' || idx.name === '_email_verify_token_1')).toBeDefined();
823+
expect(userIndexes.find(idx => idx.name === '_perishable_token' || idx.name === '_perishable_token_1')).toBeDefined();
824+
expect(roleIndexes.find(idx => idx.name === 'name_1')).toBeDefined();
825+
});
826+
});
652827
});

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,22 @@ export class MongoStorageAdapter implements StorageAdapter {
154154
this.enableSchemaHooks = !!mongoOptions.enableSchemaHooks;
155155
this.schemaCacheTtl = mongoOptions.schemaCacheTtl;
156156
this.disableIndexFieldValidation = !!mongoOptions.disableIndexFieldValidation;
157-
for (const key of ['enableSchemaHooks', 'schemaCacheTtl', 'maxTimeMS', 'disableIndexFieldValidation']) {
158-
delete mongoOptions[key];
157+
// Remove Parse Server-specific options that should not be passed to MongoDB client
158+
// Note: We only delete from this._mongoOptions, not from the original mongoOptions object,
159+
// because other components (like DatabaseController) need access to these options
160+
for (const key of [
161+
'enableSchemaHooks',
162+
'schemaCacheTtl',
163+
'maxTimeMS',
164+
'disableIndexFieldValidation',
165+
'createIndexUserUsername',
166+
'createIndexUserUsernameCaseInsensitive',
167+
'createIndexUserEmail',
168+
'createIndexUserEmailCaseInsensitive',
169+
'createIndexUserEmailVerifyToken',
170+
'createIndexUserPasswordResetToken',
171+
'createIndexRoleName',
172+
]) {
159173
delete this._mongoOptions[key];
160174
}
161175
}

src/Controllers/DatabaseController.js

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1738,50 +1738,66 @@ class DatabaseController {
17381738
await this.loadSchema().then(schema => schema.enforceClassExists('_Role'));
17391739
await this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency'));
17401740

1741-
await this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']).catch(error => {
1742-
logger.warn('Unable to ensure uniqueness for usernames: ', error);
1743-
throw error;
1744-
});
1741+
const databaseOptions = this.options.databaseOptions || {};
1742+
1743+
if (databaseOptions.createIndexUserUsername !== false) {
1744+
await this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']).catch(error => {
1745+
logger.warn('Unable to ensure uniqueness for usernames: ', error);
1746+
throw error;
1747+
});
1748+
}
17451749

17461750
if (!this.options.enableCollationCaseComparison) {
1751+
if (databaseOptions.createIndexUserUsernameCaseInsensitive !== false) {
1752+
await this.adapter
1753+
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
1754+
.catch(error => {
1755+
logger.warn('Unable to create case insensitive username index: ', error);
1756+
throw error;
1757+
});
1758+
}
1759+
1760+
if (databaseOptions.createIndexUserEmailCaseInsensitive !== false) {
1761+
await this.adapter
1762+
.ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true)
1763+
.catch(error => {
1764+
logger.warn('Unable to create case insensitive email index: ', error);
1765+
throw error;
1766+
});
1767+
}
1768+
}
1769+
1770+
if (databaseOptions.createIndexUserEmail !== false) {
1771+
await this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']).catch(error => {
1772+
logger.warn('Unable to ensure uniqueness for user email addresses: ', error);
1773+
throw error;
1774+
});
1775+
}
1776+
1777+
if (databaseOptions.createIndexUserEmailVerifyToken !== false) {
17471778
await this.adapter
1748-
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
1779+
.ensureIndex('_User', requiredUserFields, ['_email_verify_token'], '_email_verify_token', false)
17491780
.catch(error => {
1750-
logger.warn('Unable to create case insensitive username index: ', error);
1781+
logger.warn('Unable to create index for email verification token: ', error);
17511782
throw error;
17521783
});
1784+
}
17531785

1786+
if (databaseOptions.createIndexUserPasswordResetToken !== false) {
17541787
await this.adapter
1755-
.ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true)
1788+
.ensureIndex('_User', requiredUserFields, ['_perishable_token'], '_perishable_token', false)
17561789
.catch(error => {
1757-
logger.warn('Unable to create case insensitive email index: ', error);
1790+
logger.warn('Unable to create index for password reset token: ', error);
17581791
throw error;
17591792
});
17601793
}
17611794

1762-
await this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']).catch(error => {
1763-
logger.warn('Unable to ensure uniqueness for user email addresses: ', error);
1764-
throw error;
1765-
});
1766-
1767-
await this.adapter
1768-
.ensureIndex('_User', requiredUserFields, ['_email_verify_token'], '_email_verify_token', false)
1769-
.catch(error => {
1770-
logger.warn('Unable to create index for email verification token: ', error);
1771-
throw error;
1772-
});
1773-
1774-
await this.adapter
1775-
.ensureIndex('_User', requiredUserFields, ['_perishable_token'], '_perishable_token', false)
1776-
.catch(error => {
1777-
logger.warn('Unable to create index for password reset token: ', error);
1795+
if (databaseOptions.createIndexRoleName !== false) {
1796+
await this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']).catch(error => {
1797+
logger.warn('Unable to ensure uniqueness for role name: ', error);
17781798
throw error;
17791799
});
1780-
1781-
await this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']).catch(error => {
1782-
logger.warn('Unable to ensure uniqueness for role name: ', error);
1783-
throw error;
1784-
});
1800+
}
17851801

17861802
await this.adapter
17871803
.ensureUniqueness('_Idempotency', requiredIdempotencyFields, ['reqId'])

src/Options/Definitions.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,55 @@ module.exports.DatabaseOptions = {
11011101
'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.',
11021102
action: parsers.numberParser('connectTimeoutMS'),
11031103
},
1104+
createIndexRoleName: {
1105+
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_ROLE_NAME',
1106+
help:
1107+
'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.',
1108+
action: parsers.booleanParser,
1109+
default: true,
1110+
},
1111+
createIndexUserEmail: {
1112+
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL',
1113+
help:
1114+
'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.',
1115+
action: parsers.booleanParser,
1116+
default: true,
1117+
},
1118+
createIndexUserEmailCaseInsensitive: {
1119+
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL_CASE_INSENSITIVE',
1120+
help:
1121+
'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.',
1122+
action: parsers.booleanParser,
1123+
default: true,
1124+
},
1125+
createIndexUserEmailVerifyToken: {
1126+
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_EMAIL_VERIFY_TOKEN',
1127+
help:
1128+
'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.',
1129+
action: parsers.booleanParser,
1130+
default: true,
1131+
},
1132+
createIndexUserPasswordResetToken: {
1133+
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_PASSWORD_RESET_TOKEN',
1134+
help:
1135+
'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.',
1136+
action: parsers.booleanParser,
1137+
default: true,
1138+
},
1139+
createIndexUserUsername: {
1140+
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_USERNAME',
1141+
help:
1142+
'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.',
1143+
action: parsers.booleanParser,
1144+
default: true,
1145+
},
1146+
createIndexUserUsernameCaseInsensitive: {
1147+
env: 'PARSE_SERVER_DATABASE_CREATE_INDEX_USER_USERNAME_CASE_INSENSITIVE',
1148+
help:
1149+
'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.',
1150+
action: parsers.booleanParser,
1151+
default: true,
1152+
},
11041153
disableIndexFieldValidation: {
11051154
env: 'PARSE_SERVER_DATABASE_DISABLE_INDEX_FIELD_VALIDATION',
11061155
help:

0 commit comments

Comments
 (0)