diff --git a/packages/entity/src/EntityConfiguration.ts b/packages/entity/src/EntityConfiguration.ts index cf2c4dcfe..3ac25e2d5 100644 --- a/packages/entity/src/EntityConfiguration.ts +++ b/packages/entity/src/EntityConfiguration.ts @@ -75,7 +75,7 @@ export default class EntityConfiguration> { // external schema is a Record to typecheck that all fields have FieldDefinitions, // but internally the most useful representation is a map for lookups - // TODO(wschurman): validate schema + EntityConfiguration.validateSchema(schema); this.schema = new Map(Object.entries(schema)); this.cacheableKeys = EntityConfiguration.computeCacheableKeys(this.schema); @@ -85,6 +85,17 @@ export default class EntityConfiguration> { this.dbToEntityFieldsKeyMapping = invertMap(this.entityToDBFieldsKeyMapping); } + private static validateSchema>(schema: TFields): void { + const disallowedFieldsKeys = Object.getOwnPropertyNames(Object.prototype); + for (const disallowedFieldsKey of disallowedFieldsKeys) { + if (Object.hasOwn(schema, disallowedFieldsKey)) { + throw new Error( + `Entity field name not allowed to prevent conflicts with standard Object prototype fields: ${disallowedFieldsKey}` + ); + } + } + } + private static computeCacheableKeys( schema: ReadonlyMap> ): ReadonlySet { diff --git a/packages/entity/src/__tests__/EntityConfiguration-test.ts b/packages/entity/src/__tests__/EntityConfiguration-test.ts new file mode 100644 index 000000000..f9cfe9540 --- /dev/null +++ b/packages/entity/src/__tests__/EntityConfiguration-test.ts @@ -0,0 +1,118 @@ +import EntityConfiguration from '../EntityConfiguration'; +import { UUIDField, StringField } from '../EntityFields'; + +describe(EntityConfiguration, () => { + describe('when valid', () => { + type BlahT = { + id: string; + cacheable: string; + uniqueButNotCacheable: string; + }; + + type Blah2T = { + id: string; + }; + + const blahEntityConfiguration = new EntityConfiguration({ + idField: 'id', + tableName: 'blah_table', + schema: { + id: new UUIDField({ + columnName: 'id', + }), + cacheable: new StringField({ + columnName: 'cacheable', + cache: true, + }), + uniqueButNotCacheable: new StringField({ + columnName: 'unique_but_not_cacheable', + }), + }, + databaseAdapterFlavor: 'postgres', + cacheAdapterFlavor: 'redis', + }); + + it('returns correct fields', () => { + expect(blahEntityConfiguration.idField).toEqual('id'); + expect(blahEntityConfiguration.tableName).toEqual('blah_table'); + expect(blahEntityConfiguration.databaseAdapterFlavor).toEqual('postgres'); + expect(blahEntityConfiguration.cacheAdapterFlavor).toEqual('redis'); + }); + + it('filters cacheable fields', () => { + expect(blahEntityConfiguration.cacheableKeys).toEqual(new Set(['cacheable'])); + }); + + describe('cache key version', () => { + it('defaults to 0', () => { + const entityConfiguration = new EntityConfiguration({ + idField: 'id', + tableName: 'blah', + schema: { + id: new UUIDField({ + columnName: 'id', + }), + }, + databaseAdapterFlavor: 'postgres', + cacheAdapterFlavor: 'redis', + }); + expect(entityConfiguration.cacheKeyVersion).toEqual(0); + }); + + it('sets to custom version', () => { + const entityConfiguration = new EntityConfiguration({ + idField: 'id', + tableName: 'blah', + schema: { + id: new UUIDField({ + columnName: 'id', + }), + }, + databaseAdapterFlavor: 'postgres', + cacheAdapterFlavor: 'redis', + cacheKeyVersion: 100, + }); + expect(entityConfiguration.cacheKeyVersion).toEqual(100); + }); + }); + }); + + describe('validation', () => { + describe('disallows keys of JS Object prototype for safety', () => { + test.each([ + 'constructor', + '__defineGetter__', + '__defineSetter__', + 'hasOwnProperty', + '__lookupGetter__', + '__lookupSetter__', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toString', + 'valueOf', + '__proto__', + 'toLocaleString', + ])('disallows %p as field key', (keyName) => { + expect( + () => + new EntityConfiguration({ + idField: 'id', + tableName: 'blah_table', + schema: { + id: new UUIDField({ + columnName: 'id', + }), + [keyName]: new StringField({ + columnName: 'any', + }), + }, + databaseAdapterFlavor: 'postgres', + cacheAdapterFlavor: 'redis', + }) + ).toThrow( + `Entity field name not allowed to prevent conflicts with standard Object prototype fields: ${keyName}` + ); + }); + }); + }); +}); diff --git a/packages/entity/src/__tests__/EntityDataConfiguration-test.ts b/packages/entity/src/__tests__/EntityDataConfiguration-test.ts deleted file mode 100644 index d6cd02c4b..000000000 --- a/packages/entity/src/__tests__/EntityDataConfiguration-test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import EntityConfiguration from '../EntityConfiguration'; -import { UUIDField, StringField } from '../EntityFields'; - -describe(EntityConfiguration, () => { - type BlahT = { - id: string; - cacheable: string; - uniqueButNotCacheable: string; - }; - - type Blah2T = { - id: string; - }; - - const blahEntityConfiguration = new EntityConfiguration({ - idField: 'id', - tableName: 'blah_table', - schema: { - id: new UUIDField({ - columnName: 'id', - }), - cacheable: new StringField({ - columnName: 'cacheable', - cache: true, - }), - uniqueButNotCacheable: new StringField({ - columnName: 'unique_but_not_cacheable', - }), - }, - databaseAdapterFlavor: 'postgres', - cacheAdapterFlavor: 'redis', - }); - - it('returns correct fields', () => { - expect(blahEntityConfiguration.idField).toEqual('id'); - expect(blahEntityConfiguration.tableName).toEqual('blah_table'); - expect(blahEntityConfiguration.databaseAdapterFlavor).toEqual('postgres'); - expect(blahEntityConfiguration.cacheAdapterFlavor).toEqual('redis'); - }); - - it('filters cacheable fields', () => { - expect(blahEntityConfiguration.cacheableKeys).toEqual(new Set(['cacheable'])); - }); - - describe('cache key version', () => { - it('defaults to 0', () => { - const entityConfiguration = new EntityConfiguration({ - idField: 'id', - tableName: 'blah', - schema: { - id: new UUIDField({ - columnName: 'id', - }), - }, - databaseAdapterFlavor: 'postgres', - cacheAdapterFlavor: 'redis', - }); - expect(entityConfiguration.cacheKeyVersion).toEqual(0); - }); - - it('sets to custom version', () => { - const entityConfiguration = new EntityConfiguration({ - idField: 'id', - tableName: 'blah', - schema: { - id: new UUIDField({ - columnName: 'id', - }), - }, - databaseAdapterFlavor: 'postgres', - cacheAdapterFlavor: 'redis', - cacheKeyVersion: 100, - }); - expect(entityConfiguration.cacheKeyVersion).toEqual(100); - }); - }); -}); diff --git a/packages/entity/src/errors/EntityError.ts b/packages/entity/src/errors/EntityError.ts index 883b4802d..d1e0717ea 100644 --- a/packages/entity/src/errors/EntityError.ts +++ b/packages/entity/src/errors/EntityError.ts @@ -24,7 +24,7 @@ export default abstract class EntityError extends ES6Error { public abstract readonly state: EntityErrorState; public abstract readonly code: EntityErrorCode; - constructor(message: string, public readonly cause?: Error) { + constructor(message: string, public override readonly cause?: Error) { super(message); } } diff --git a/tsconfig.json b/tsconfig.json index b95ea8e36..f50ab8b8a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "target": "es2021", - "lib": ["es2021"], + "target": "es2022", + "lib": ["es2023"], "module": "commonjs", "sourceMap": true, "moduleResolution": "node", diff --git a/yarn.lock b/yarn.lock index 653af3df7..e0133aad0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -675,37 +675,38 @@ integrity sha512-reebgVwjf8VfZxSXU7e+UjpXGwcUTIMpWR9FY54Oh70ulhXrQiZei62B4D9bH3SVYMwnDGzifHJ8INRrJ+0L1g== "@expo/entity-cache-adapter-local-memory@file:packages/entity-cache-adapter-local-memory": - version "0.31.1" + version "0.35.0" dependencies: lru-cache "^6.0.0" "@expo/entity-cache-adapter-redis@file:packages/entity-cache-adapter-redis": - version "0.31.1" + version "0.35.0" "@expo/entity-database-adapter-knex@file:packages/entity-database-adapter-knex": - version "0.31.1" + version "0.35.0" dependencies: knex "^2.4.2" "@expo/entity-ip-address-field@file:packages/entity-ip-address-field": - version "0.31.1" + version "0.35.0" dependencies: ip-address "^8.1.0" "@expo/entity-secondary-cache-local-memory@file:packages/entity-secondary-cache-local-memory": - version "0.31.1" + version "0.35.0" "@expo/entity-secondary-cache-redis@file:packages/entity-secondary-cache-redis": - version "0.31.1" + version "0.35.0" "@expo/entity@file:packages/entity": - version "0.31.1" + version "0.35.0" dependencies: "@expo/results" "^1.0.0" dataloader "^2.0.0" es6-error "^4.1.1" invariant "^2.2.4" uuid "^8.3.0" + uuidv7 "^1.0.0" "@expo/results@^1.0.0": version "1.0.0" @@ -8661,6 +8662,11 @@ uuid@^9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== +uuidv7@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/uuidv7/-/uuidv7-1.0.0.tgz#b097dd0d48c5e48edf661199e033f10ebee08cda" + integrity sha512-XkvPwTtSmYwxIE1FSYQTYg79zHL1ZWV5vM/Qyl9ahXCU8enOPPA4bTjzvafvYUB7l2+miv4EqK/qEe75cOXIdA== + v8-compile-cache@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"