forked from wovalle/fireorm
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(32): support multiple subcollections of the same type (#45)
* feat(metadata): index subcollections on entityConstructor, parentEntityConstructor and parentName * feat(metadata): expand metadata types with generics * refactor(metadata): collection registration error handling * test(metadata): fix and add unit tests for MetadataStorage to verify collection type agnosticism * chore: update vscode debug cofiguration * feat(multi-sub-col): add indexing by collection name to AbstractFirestoreRepository * feat(multi-sub-col): add name support to BaseFirestoreRepository and fix tests * feat(multi-sub-col): update SubCollection decorator to pre-register metadata and fix tests * feat(multi-sub-col): update Collection decorator with recursion for subcollections and fix tests * chore: update tsconfig.json with decorator support * refactor(errors): extend NoMetadataError * refactor: update serializeEntity to remove SubCollectionMetadata * feat(multi-sub-col): add collection name support * refactor: add isConstructor type guard * feat(multi-sub-col): misc collection name support and fxns * feat(multi-sub-col): update CustomRepository decorator with collection name support and fix tests * feat(multi-sub-col): update collection and repository registration, fix tests * feat(multi-sub-col): update atomic functionality to support collection names, fix tests * fix(integration): fix simple repo integration spec * fix(integration): fix custom repo integration spec * test(integration): fix subcollections integration spec * fix(integration): fix transactions integration spec * fix(integration): fix batches integration spec * fix(integration): fix document references integration spec * fix(integration): fix validations integration spec * fix(integration): fix serialized properties integration spec * fix(integration): fix ignore properties integration spec * fix(integration): fix queries integration spec and rename * style: prettier and lint fix * refactor(metadata): organize errors and update tests * test(metadata): add unit test coverage for private methods * style: prettier formatting * test(helpers): update error types and add coverage for helpers * chore: cleanup utils * test: add missing unit tests for AbstractFirestoreRepository * style: prettier formatting * fix(sonarcloud): minor code smells * style: prettier format * test(skipped): resolve skipped tests
- Loading branch information
Showing
40 changed files
with
1,507 additions
and
446 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,289 @@ | ||
import { CollectionReference, Transaction } from '@google-cloud/firestore'; | ||
import { getMetadataStorage } from './MetadataUtils'; | ||
import { FullCollectionMetadata, MetadataStorageConfig } from './MetadataStorage'; | ||
import { NoFirestoreError, NoMetadataError, NoParentPropertyKeyError } from './Errors'; | ||
import { IEntity } from './types'; | ||
import { AbstractFirestoreRepository } from './AbstractFirestoreRepository'; | ||
import { FirestoreTransaction } from './Transaction/FirestoreTransaction'; | ||
|
||
jest.mock('./MetadataUtils'); | ||
jest.mock('./helpers'); | ||
jest.mock('./Transaction/FirestoreTransaction'); | ||
|
||
interface FirestoreGeoPoint { | ||
latitude: number; | ||
longitude: number; | ||
} | ||
|
||
interface FirestoreDocumentReference { | ||
id: string; | ||
path: string; | ||
} | ||
|
||
interface FirestoreData { | ||
timestampField: { toDate: () => Date }; | ||
geoPointField: FirestoreGeoPoint; | ||
documentReferenceField: FirestoreDocumentReference; | ||
nestedObject: { | ||
timestampField: { toDate: () => Date }; | ||
}; | ||
} | ||
|
||
interface TransformedData { | ||
timestampField: Date; | ||
geoPointField: { latitude: number; longitude: number }; | ||
documentReferenceField: { id: string; path: string }; | ||
nestedObject: { | ||
timestampField: Date; | ||
}; | ||
} | ||
|
||
class TestEntity implements IEntity { | ||
id!: string; | ||
} | ||
|
||
describe('AbstractFirestoreRepository', () => { | ||
let collectionRefMock: jest.Mocked<CollectionReference>; | ||
let getCollectionMock: jest.Mock; | ||
let firestoreRefMock: any; | ||
let getRepositoryMock: jest.Mock; | ||
let firestoreTransactionMock: jest.Mocked<FirestoreTransaction>; | ||
|
||
beforeEach(() => { | ||
collectionRefMock = { | ||
doc: jest.fn().mockReturnThis(), | ||
collection: jest.fn().mockReturnThis(), | ||
add: jest.fn().mockResolvedValue({ id: 'new-id' }), | ||
} as unknown as jest.Mocked<CollectionReference>; | ||
|
||
getCollectionMock = jest.fn(); | ||
firestoreRefMock = { | ||
collection: jest.fn().mockReturnValue(collectionRefMock), | ||
}; | ||
|
||
(getMetadataStorage as jest.Mock).mockReturnValue({ | ||
getCollection: getCollectionMock, | ||
config: {} as MetadataStorageConfig, | ||
firestoreRef: firestoreRefMock, | ||
}); | ||
|
||
getRepositoryMock = jest.fn(); | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const helpers = require('./helpers'); | ||
helpers.getRepository = getRepositoryMock; | ||
getRepositoryMock.mockReturnValue({ someMethod: jest.fn() }); | ||
|
||
firestoreTransactionMock = { | ||
getRepository: jest.fn().mockReturnValue({ someMethod: jest.fn() }), | ||
} as unknown as jest.Mocked<FirestoreTransaction>; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const FirestoreTransaction = require('./Transaction/FirestoreTransaction'); | ||
(FirestoreTransaction.FirestoreTransaction as jest.Mock).mockImplementation( | ||
() => firestoreTransactionMock | ||
); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
jest.resetModules(); | ||
}); | ||
|
||
class TestRepository extends AbstractFirestoreRepository<TestEntity> { | ||
execute = jest.fn(); | ||
findById = jest.fn(); | ||
create = jest.fn(); | ||
update = jest.fn(); | ||
delete = jest.fn(); | ||
|
||
// Expose the protected methods for testing | ||
public transformTypes(obj: FirestoreData): TransformedData { | ||
const transformed = this.transformFirestoreTypes(obj as unknown as Record<string, unknown>); | ||
return transformed as unknown as TransformedData; | ||
} | ||
|
||
public initializeSubCollectionsPublic( | ||
entity: TestEntity, | ||
tran?: Transaction, | ||
tranRefStorage?: any | ||
) { | ||
return this.initializeSubCollections(entity, tran, tranRefStorage); | ||
} | ||
} | ||
|
||
describe('Constructor', () => { | ||
it('should throw NoFirestoreError if firestoreRef is not set', () => { | ||
(getMetadataStorage as jest.Mock).mockReturnValueOnce({ | ||
getCollection: getCollectionMock, | ||
config: {} as MetadataStorageConfig, | ||
firestoreRef: undefined, | ||
}); | ||
|
||
expect(() => new TestRepository('path', 'TestEntity')).toThrow(NoFirestoreError); | ||
}); | ||
|
||
it('should throw NoMetadataError if no Metadata is not found for the specified collection', () => { | ||
getCollectionMock.mockReturnValueOnce(undefined); | ||
|
||
expect(() => new TestRepository('path', 'TestEntity')).toThrow(NoMetadataError); | ||
}); | ||
|
||
it('should initialize class properties correctly', () => { | ||
const colMetadataMock = { | ||
entityConstructor: TestEntity, | ||
name: 'TestEntity', | ||
segments: ['TestEntity'], | ||
parentProps: null, | ||
subCollections: [], | ||
} as FullCollectionMetadata; | ||
|
||
getCollectionMock.mockReturnValueOnce(colMetadataMock); | ||
|
||
const repository = new TestRepository('path', 'TestEntity'); | ||
|
||
expect((repository as any).colMetadata).toBe(colMetadataMock); | ||
expect((repository as any).path).toBe('path'); | ||
expect((repository as any).name).toBe('TestEntity'); | ||
expect((repository as any).firestoreColRef).toBe(collectionRefMock); | ||
}); | ||
}); | ||
|
||
describe('transformFirestoreTypes', () => { | ||
it('should transform Firestore types correctly', () => { | ||
const colMetadataMock = { | ||
entityConstructor: TestEntity, | ||
name: 'TestEntity', | ||
segments: ['TestEntity'], | ||
parentProps: null, | ||
subCollections: [], | ||
} as FullCollectionMetadata; | ||
|
||
getCollectionMock.mockReturnValueOnce(colMetadataMock); | ||
|
||
const repository = new TestRepository('path', 'TestEntity'); | ||
|
||
const firestoreData: FirestoreData = { | ||
timestampField: { | ||
toDate: () => new Date('2020-01-01T00:00:00Z'), | ||
}, | ||
geoPointField: { | ||
latitude: 10, | ||
longitude: 20, | ||
}, | ||
documentReferenceField: { | ||
id: 'docId', | ||
path: 'path/to/doc', | ||
}, | ||
nestedObject: { | ||
timestampField: { | ||
toDate: () => new Date('2020-01-01T00:00:00Z'), | ||
}, | ||
}, | ||
}; | ||
|
||
// Explicitly cast the transformed data to the correct type | ||
const transformedData = repository.transformTypes(firestoreData); | ||
|
||
expect(transformedData.timestampField).toEqual(new Date('2020-01-01T00:00:00Z')); | ||
expect(transformedData.geoPointField).toEqual({ latitude: 10, longitude: 20 }); | ||
expect(transformedData.documentReferenceField).toEqual({ id: 'docId', path: 'path/to/doc' }); | ||
expect(transformedData.nestedObject.timestampField).toEqual(new Date('2020-01-01T00:00:00Z')); | ||
}); | ||
}); | ||
|
||
describe('initializeSubCollections', () => { | ||
it('should initialize subcollections correctly', () => { | ||
const colMetadataMock = { | ||
entityConstructor: TestEntity, | ||
name: 'TestEntity', | ||
segments: ['TestEntity'], | ||
parentProps: null, | ||
subCollections: [ | ||
{ | ||
name: 'subCollection', | ||
parentProps: { | ||
parentPropertyKey: 'subCollectionRepository', | ||
}, | ||
}, | ||
], | ||
} as FullCollectionMetadata; | ||
|
||
getCollectionMock.mockReturnValueOnce(colMetadataMock); | ||
|
||
const repository = new TestRepository('path', 'TestEntity'); | ||
|
||
const entity = new TestEntity(); | ||
entity.id = 'entityId'; | ||
|
||
repository.initializeSubCollectionsPublic(entity); | ||
|
||
expect((entity as any).subCollectionRepository).toBeDefined(); | ||
expect(getRepositoryMock).toHaveBeenCalledWith('path/entityId/subCollection'); | ||
}); | ||
|
||
it('should throw NoParentPropertyKeyError if parentPropertyKey is not defined', () => { | ||
const colMetadataMock = { | ||
entityConstructor: TestEntity, | ||
name: 'TestEntity', | ||
segments: ['TestEntity'], | ||
parentProps: null, | ||
subCollections: [ | ||
{ | ||
name: 'subCollection', | ||
parentProps: null, | ||
}, | ||
], | ||
} as FullCollectionMetadata; | ||
|
||
getCollectionMock.mockReturnValueOnce(colMetadataMock); | ||
|
||
const repository = new TestRepository('path', 'TestEntity'); | ||
|
||
const entity = new TestEntity(); | ||
entity.id = 'entityId'; | ||
|
||
expect(() => repository.initializeSubCollectionsPublic(entity)).toThrow( | ||
NoParentPropertyKeyError | ||
); | ||
}); | ||
|
||
it('should initialize subcollections correctly within a transaction', () => { | ||
const colMetadataMock = { | ||
entityConstructor: TestEntity, | ||
name: 'TestEntity', | ||
segments: ['TestEntity'], | ||
parentProps: null, | ||
subCollections: [ | ||
{ | ||
name: 'subCollection', | ||
parentProps: { | ||
parentPropertyKey: 'subCollectionRepository', | ||
}, | ||
}, | ||
], | ||
} as FullCollectionMetadata; | ||
|
||
getCollectionMock.mockReturnValueOnce(colMetadataMock); | ||
|
||
const repository = new TestRepository('path', 'TestEntity'); | ||
|
||
const entity = new TestEntity(); | ||
entity.id = 'entityId'; | ||
|
||
const tranRefStorageMock = { add: jest.fn() }; | ||
|
||
repository.initializeSubCollectionsPublic(entity, {} as Transaction, tranRefStorageMock); | ||
|
||
expect((entity as any).subCollectionRepository).toBeDefined(); | ||
expect(firestoreTransactionMock.getRepository).toHaveBeenCalledWith( | ||
'path/entityId/subCollection' | ||
); | ||
expect(tranRefStorageMock.add).toHaveBeenCalledWith({ | ||
parentPropertyKey: 'subCollectionRepository', | ||
path: 'path/entityId/subCollection', | ||
entity, | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.