Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(32): support multiple subcollections of the same type #45

Merged
merged 39 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2e8670f
feat(metadata): index subcollections on entityConstructor, parentEnti…
elersong Jul 16, 2024
c4579fb
feat(metadata): expand metadata types with generics
elersong Jul 16, 2024
f6ad3b8
refactor(metadata): collection registration error handling
elersong Jul 16, 2024
62953a2
test(metadata): fix and add unit tests for MetadataStorage to verify …
elersong Jul 16, 2024
74ea128
chore: update vscode debug cofiguration
elersong Jul 21, 2024
08d0396
feat(multi-sub-col): add indexing by collection name to AbstractFires…
elersong Jul 21, 2024
3fad654
feat(multi-sub-col): add name support to BaseFirestoreRepository and …
elersong Jul 21, 2024
31512ef
feat(multi-sub-col): update SubCollection decorator to pre-register m…
elersong Jul 21, 2024
97a6d20
feat(multi-sub-col): update Collection decorator with recursion for s…
elersong Jul 21, 2024
a90374a
chore: update tsconfig.json with decorator support
elersong Jul 21, 2024
58e81ea
refactor(errors): extend NoMetadataError
elersong Jul 21, 2024
bb0e9f1
refactor: update serializeEntity to remove SubCollectionMetadata
elersong Jul 21, 2024
566293d
feat(multi-sub-col): add collection name support
elersong Jul 21, 2024
b0bc1cd
refactor: add isConstructor type guard
elersong Jul 21, 2024
a616053
feat(multi-sub-col): misc collection name support and fxns
elersong Jul 21, 2024
c55425e
feat(multi-sub-col): update CustomRepository decorator with collectio…
elersong Jul 21, 2024
a9ddff1
feat(multi-sub-col): update collection and repository registration, f…
elersong Jul 21, 2024
a7a5eee
feat(multi-sub-col): update atomic functionality to support collectio…
elersong Jul 21, 2024
1a2bced
fix(integration): fix simple repo integration spec
elersong Jul 22, 2024
087a0e9
fix(integration): fix custom repo integration spec
elersong Jul 22, 2024
9f6c942
test(integration): fix subcollections integration spec
elersong Jul 22, 2024
4fc64c8
fix(integration): fix transactions integration spec
elersong Jul 22, 2024
c95945c
fix(integration): fix batches integration spec
elersong Jul 22, 2024
b8467a1
fix(integration): fix document references integration spec
elersong Jul 22, 2024
d37e33c
fix(integration): fix validations integration spec
elersong Jul 22, 2024
48a1336
fix(integration): fix serialized properties integration spec
elersong Jul 22, 2024
e1bc2d9
fix(integration): fix ignore properties integration spec
elersong Jul 22, 2024
b5b3c83
fix(integration): fix queries integration spec and rename
elersong Jul 22, 2024
1f4cad3
style: prettier and lint fix
elersong Jul 23, 2024
e247405
refactor(metadata): organize errors and update tests
elersong Jul 23, 2024
5f94abc
test(metadata): add unit test coverage for private methods
elersong Jul 23, 2024
dba485e
style: prettier formatting
elersong Jul 23, 2024
fb6fa23
test(helpers): update error types and add coverage for helpers
elersong Jul 24, 2024
9e83fa4
chore: cleanup utils
elersong Jul 24, 2024
f669817
test: add missing unit tests for AbstractFirestoreRepository
elersong Jul 25, 2024
5bf3264
style: prettier formatting
elersong Jul 25, 2024
a75c613
fix(sonarcloud): minor code smells
elersong Jul 25, 2024
bdb615d
style: prettier format
elersong Jul 25, 2024
0b8615f
test(skipped): resolve skipped tests
elersong Jul 25, 2024
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
30 changes: 27 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [

{
"type": "node",
"request": "launch",
"name": "Launch current file w/ ts-node",
"protocol": "inspector",
"args": ["${relativeFile}"],
"cwd": "${workspaceRoot}",
"runtimeArgs": ["-r", "ts-node/register"],
Expand All @@ -26,8 +26,32 @@
"--verbose"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
"name": "Debug Jest Tests",
"program": "${workspaceRoot}/node_modules/.bin/jest",
"args": [
"--runInBand"
],
"runtimeArgs": [
"--inspect-brk"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
"name": "Debug Current Jest Unit Test",
"program": "${workspaceRoot}/node_modules/.bin/jest",
"args": ["${file}"],
"runtimeArgs": ["--inspect-brk"],
"cwd": "${workspaceRoot}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
289 changes: 289 additions & 0 deletions src/AbstractFirestoreRepository.spec.ts
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,
});
});
});
});
Loading