diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..be93552 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +FROM node:18.12 + +ARG USER_NAME=user +ARG USER_UID=1000 +ARG USER_GID=$USER_UID +ARG GITHUB_TOKEN + +# Install necessary apps +RUN apt-get update \ + && apt-get install -y git fish sudo + +# Create the user +RUN groupadd --gid ${USER_GID} ${USER_NAME} \ + && useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USER_NAME} \ + && echo $USER_NAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/${USER_NAME} \ + && chmod 0440 /etc/sudoers.d/${USER_NAME} +USER ${USER_NAME} + +# Copy files +WORKDIR /workspaces +COPY --chown=${USER_UID}:${USER_GID} . . +RUN npm install + +# Infinite development loop +CMD ["/bin/bash", "-c", "while true; do sleep 1000; done"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1deb0e2..8ed0197 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,15 +1,25 @@ { - "extensions": [ - "GitHub.vscode-pull-request-github", - "stackbreak.comment-divider", - "dbaeumer.vscode-eslint", - "GitHub.copilot", - "Orta.vscode-jest", - "yzhang.markdown-all-in-one", - "bierner.markdown-mermaid", - "eg2.vscode-npm-script", - "howardzuo.vscode-npm-dependency", - "craig-simko.open-stryker-report", - "jasonnutter.search-node-modules" - ] + "name": "Framework", + "dockerComposeFile": [ + "docker-compose.yml" + ], + "service": "framework", + "workspaceFolder": "/workspaces", + "customizations": { + "vscode": { + "extensions": [ + "GitHub.vscode-pull-request-github", + "stackbreak.comment-divider", + "dbaeumer.vscode-eslint", + "GitHub.copilot", + "Orta.vscode-jest", + "yzhang.markdown-all-in-one", + "bierner.markdown-mermaid", + "eg2.vscode-npm-script", + "howardzuo.vscode-npm-dependency", + "craig-simko.open-stryker-report", + "jasonnutter.search-node-modules" + ] + } + } } \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..0767579 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,14 @@ +name: framework + +services: + framework: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + args: + - USER_NAME=${DEVCONTAINER_USER_NAME} + - USER_UID=${DEVCONTAINER_USER_ID:-1000} + - USER_GID=${DEVCONTAINER_USER_GID:-1000} + - GITHUB_TOKEN=${GITHUB_PAT_TOKEN} + volumes: + - ..:/workspaces diff --git a/.vscode/settings.json b/.vscode/settings.json index 87ba1a4..efbc970 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,7 +13,7 @@ /* linter */ /* -------------------------------------------------------------------------- */ "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "eslint.validate": [ "javascript", diff --git a/lib/domain/models/Builder.ts b/lib/domain/models/Builder.ts index 69cbbcc..36b5486 100644 --- a/lib/domain/models/Builder.ts +++ b/lib/domain/models/Builder.ts @@ -1,4 +1,4 @@ -import { Result } from '@lib/core' +import { Result } from '../../core' /** * Builds a new instance of the specified type or returns a failure result. diff --git a/lib/persistence/Query.ts b/lib/persistence/Query.ts index 39a7171..c70a047 100644 --- a/lib/persistence/Query.ts +++ b/lib/persistence/Query.ts @@ -1,4 +1,4 @@ -import { Aggregate, AnyIdentity } from '@lib/domain/models' +import { Aggregate, AnyIdentity } from '../domain' /** * Comparison operators. These are used to compare a field with a value. diff --git a/lib/persistence/Repository.ts b/lib/persistence/Repository.ts index 05a10e7..d2a1d2b 100644 --- a/lib/persistence/Repository.ts +++ b/lib/persistence/Repository.ts @@ -1,5 +1,5 @@ -import { Aggregate, AnyIdentity } from '@lib/domain/models' -import { Query } from '@lib/persistence' +import { Aggregate, AnyIdentity } from '../domain/models' +import { Query } from '../persistence' export interface ResultSetSlice { start: number diff --git a/lib/persistence/memory/InMemoryQueryProcessor.ts b/lib/persistence/memory/InMemoryQueryProcessor.ts index 243819a..60f37ac 100644 --- a/lib/persistence/memory/InMemoryQueryProcessor.ts +++ b/lib/persistence/memory/InMemoryQueryProcessor.ts @@ -1,4 +1,4 @@ -import { Aggregate, AnyIdentity, Identity, Value } from '@lib/domain/models' +import { Aggregate, AnyIdentity, Identity, Value } from '../../domain/models' import { Binding, Expression, LogicalOperators, Operators, Predicate, Query } from '../Query' export class InMemoryQueryProcessor< @@ -72,14 +72,14 @@ export class InMemoryQueryProcessor< return a.filter(x => x.includes(b)).length > 0 } else if (typeof a === 'string') { // Stryker disable next-line all - const normalize = (str) => str.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '') + const normalize = (str: string) => str.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '') return normalize(a).includes(normalize(b)) } return false }, [Operators.In]: (a, b) => { - if (a instanceof Value) { return b.findIndex(x => x.equals(a)) !== -1 } - else if (a instanceof Identity) { return b.findIndex(x => x.equals(a)) !== -1 } + if (a instanceof Value) { return b.findIndex((x: Value) => x.equals(a)) !== -1 } + else if (a instanceof Identity) { return b.findIndex((x: Identity) => x.equals(a)) !== -1 } else return b.includes(a) }, } @@ -112,7 +112,7 @@ export class InMemoryQueryProcessor< } private getFieldValue(f: Binding, o: TEntity) { - // get netsed field value by string separated by dots + // get nested field value by string separated by dots const fields = f.split('.') let value = o for (const field of fields) { diff --git a/lib/persistence/memory/InMemoryRepository.ts b/lib/persistence/memory/InMemoryRepository.ts index de6ca78..62cf0ac 100644 --- a/lib/persistence/memory/InMemoryRepository.ts +++ b/lib/persistence/memory/InMemoryRepository.ts @@ -1,4 +1,4 @@ -import { Aggregate, AnyIdentity } from '@lib/domain/models' +import { Aggregate, AnyIdentity } from '../../domain/models' import { Query } from '../Query' import { QueryOptions, Repository, ResultSet } from '../Repository' import { InMemoryQueryProcessor } from './InMemoryQueryProcessor' @@ -11,11 +11,15 @@ export class InMemoryRepository< protected processor = new InMemoryQueryProcessor() async all( - // options?: QueryOptions, + options?: QueryOptions, ): Promise> { + let result = Array.from(this.entities.values()) + + result = this.slice(result, options?.skip, options?.limit) + return new ResultSet( - Array.from(this.entities.values()), - { start: 0, count: this.entities.size } + result, + { start: options?.skip || 0, count: result.length } ) } @@ -40,8 +44,11 @@ export class InMemoryRepository< options?: QueryOptions, ): Promise> { const startIndex = options?.skip || 0 - const entities = Array.from(this.entities.values()).slice(startIndex) - const result = this.processor.execute(query, entities) + let result = this.processor + .execute(query, Array.from(this.entities.values())) + + result = this.slice(result, options?.skip, options?.limit) + return new ResultSet( result, { start: startIndex, count: result.length } ) @@ -52,4 +59,14 @@ export class InMemoryRepository< if (!isExists) { throw new Error(`Entity '${id.value}' not found`) } this.entities.delete(id.value) } + + private slice( + list: readonly TAggregate[], + skip: number | undefined, + limit: number | undefined + ) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return list.slice(skip, (skip + limit) || limit) + } } diff --git a/tests/domain/env.ts b/tests/domain/env.ts index f3062b7..9d3695d 100644 --- a/tests/domain/env.ts +++ b/tests/domain/env.ts @@ -38,4 +38,5 @@ export class Order const qb = new QueryBuilder() export const clientName = (name: string) => qb.eq('clientName', name) export const address = (street: string, city: string, zip: string,) => qb.eq('deliveryAddress', new Address(street, city, zip)) -export const price = (price: number) => qb.eq('price', price) \ No newline at end of file +export const price = (price: number) => qb.eq('price', price) +export const expensiveThan = (price: number) => qb.gt('price', price) \ No newline at end of file diff --git a/tests/persistence/memory/InMemoryRepository.spec.ts b/tests/persistence/memory/InMemoryRepository.spec.ts index 315c159..d3cfd22 100644 --- a/tests/persistence/memory/InMemoryRepository.spec.ts +++ b/tests/persistence/memory/InMemoryRepository.spec.ts @@ -56,6 +56,26 @@ describe('InMemoryRepository', () => { const result = await repository.all() expect(result.entities).toEqual([order1, order2]) }) + + it('skip', async () => { + const result = await repository.all({ skip: 1}) + expect(result.entities).toEqual([order2]) + }) + + it('limit', async () => { + const result = await repository.all({ limit: 1}) + expect(result.entities).toEqual([order1]) + }) + + it('skip and limit', async () => { + const result = await repository.all({ skip: 1, limit: 1}) + expect(result.entities).toEqual([order2]) + }) + + it('skip to much', async () => { + const result = await repository.all({ skip: 2}) + expect(result.entities).toEqual([]) + }) }) /* -------------------------------------------------------------------------- */ @@ -121,7 +141,7 @@ describe('InMemoryRepository', () => { * Second page should return empty array because there is only one entity with * client name 'John' and we already fetched it in the "arrange" section. */ - it('respects bookmark', async () => { + it('skip', async () => { // arrange: const query = clientName('John') let result = await repository.find(query)