Skip to content

Commit

Permalink
Add data store service
Browse files Browse the repository at this point in the history
  • Loading branch information
wcalderipe committed Mar 6, 2024
1 parent e8777a3 commit a310408
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 41 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ below to generate a project of your choice.

```bash
# Generate an standard JavaScript library.
npx nx g @nrwl/workspace:lib
# Generate an NestJS library.
npx nx g @nx/nest:library
# Generate an NestJS application.
npx nx g @nx/nest:application
npx nx g @nrwl/workspace:lib
# Generate an NestJS library.
npx nx g @nx/nest:library
# Generate an NestJS application.
npx nx g @nx/nest:application
```

For more information about code generation, please refer to the [NX
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { HttpStatus, Injectable } from '@nestjs/common'
import { FileSystemDataStoreRepository } from '../../persistence/repository/file-system-data-store.repository'
import { HttpDataStoreRepository } from '../../persistence/repository/http-data-store.repository'
import { DataStoreException } from '../exception/data-store.exception'
import { DataStoreRepository } from '../repository/data-store.repository'

@Injectable()
export class DataStoreRepositoryFactory {
constructor(
private fileSystemRepository: FileSystemDataStoreRepository,
private httpRepository: HttpDataStoreRepository
) {}

getRepository(url: string): DataStoreRepository {
switch (this.getProtocol(url)) {
case 'file':
return this.fileSystemRepository
case 'http':
case 'https':
return this.httpRepository
default:
throw new DataStoreException({
message: 'Data store URL protocol not supported',
suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
context: { url }
})
}
}

private getProtocol(url: string): string {
return url.split(':')[0]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { DataStoreConfiguration, EntityData, EntitySignature, FIXTURE } from '@narval/policy-engine-shared'
import { HttpModule } from '@nestjs/axios'
import { HttpStatus } from '@nestjs/common'
import { Test } from '@nestjs/testing'
import nock from 'nock'
import { FileSystemDataStoreRepository } from '../../../../../app/persistence/repository/file-system-data-store.repository'
import { HttpDataStoreRepository } from '../../../../../app/persistence/repository/http-data-store.repository'
import { withTempJsonFile } from '../../../../../shared/testing/with-temp-json-file.testing'
import { DataStoreRepositoryFactory } from '../../../factory/data-store-repository.factory'
import { DataStoreService } from '../../data-store.service'

describe(DataStoreService.name, () => {
let service: DataStoreService

const remoteDataStoreUrl = 'http://9.9.9.9:9000'

const entityDataStore: EntityData = {
entity: {
data: FIXTURE.ENTITIES
}
}

const entitySignatureStore: EntitySignature = {
entity: {
signature: 'test-signature'
}
}

beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [HttpModule],
providers: [DataStoreService, DataStoreRepositoryFactory, HttpDataStoreRepository, FileSystemDataStoreRepository]
}).compile()

service = module.get<DataStoreService>(DataStoreService)
})

describe('fetch', () => {
it('fetches data and signature from distinct stores', async () => {
nock(remoteDataStoreUrl).get('/').reply(HttpStatus.OK, entityDataStore)

await withTempJsonFile(JSON.stringify(entitySignatureStore), async (path) => {
const url = `file://${path}`
const config: DataStoreConfiguration = {
dataUrl: remoteDataStoreUrl,
signatureUrl: url,
keys: []
}

const { entity } = await service.fetch(config)

expect(entity.data).toEqual(entityDataStore.entity.data)
expect(entity.signature).toEqual(entitySignatureStore.entity.signature)
})
})
})
})
51 changes: 51 additions & 0 deletions apps/policy-engine/src/app/core/service/data-store.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { DataStoreConfiguration, entityDataSchema, entitySignatureSchema } from '@narval/policy-engine-shared'
import { HttpStatus, Injectable } from '@nestjs/common'
import { ZodObject, z } from 'zod'
import { DataStoreException } from '../exception/data-store.exception'
import { DataStoreRepositoryFactory } from '../factory/data-store-repository.factory'

@Injectable()
export class DataStoreService {
constructor(private dataStoreRepositoryFactory: DataStoreRepositoryFactory) {}

async fetch(config: DataStoreConfiguration) {
const [entityData, entitySignature] = await Promise.all([
this.fetchByUrl(config.dataUrl, entityDataSchema),
this.fetchByUrl(config.signatureUrl, entitySignatureSchema)
])

return {
entity: {
data: entityData.entity.data,
signature: entitySignature.entity.signature
}
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private async fetchByUrl<DataSchema extends ZodObject<any>>(
url: string,
schema: DataSchema
): Promise<z.infer<typeof schema>> {
const data = await this.dataStoreRepositoryFactory.getRepository(url).fetch(url)
const result = schema.safeParse(data)

if (result.success) {
return result.data
}

throw new DataStoreException({
message: 'Invalid store schema',
suggestedHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
context: {
...(schema.description ? { schema: schema.description } : {}),
url,
errors: result.error.errors.map(({ path, message, code }) => ({
path,
code,
message
}))
}
})
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
import { EntityData, FIXTURE } from '@narval/policy-engine-shared'
import { Test } from '@nestjs/testing'
import { unlink, writeFile } from 'fs/promises'
import { v4 as uuid } from 'uuid'
import { withTempJsonFile } from '../../../../../shared/testing/with-temp-json-file.testing'
import { DataStoreException } from '../../../../core/exception/data-store.exception'
import { FileSystemDataStoreRepository } from '../../file-system-data-store.repository'

const withTempJsonFile = async (data: string, thunk: (path: string) => Promise<void>) => {
const path = `./test-temp-data-store-${uuid()}.json`

await writeFile(path, data, 'utf-8')

try {
await thunk(path)
} finally {
await unlink(path)
}
}

describe(FileSystemDataStoreRepository.name, () => {
let repository: FileSystemDataStoreRepository

Expand All @@ -36,22 +23,22 @@ describe(FileSystemDataStoreRepository.name, () => {

describe('fetch', () => {
it('fetches data from a data source in the local file system', async () => {
withTempJsonFile(JSON.stringify(entityData), async (path) => {
const data = await repository.fetch(`file:${path}`)
await withTempJsonFile(JSON.stringify(entityData), async (path) => {
const data = await repository.fetch(`file://${path}`)

expect(data).toEqual(entityData)
})
})

it('throws a DataStoreException when file does not exist', async () => {
const notFoundDataStoreUrl = 'file:./this-file-does-not-exist-in-the-file-system.json'
const notFoundDataStoreUrl = 'file://./this-file-does-not-exist-in-the-file-system.json'

await expect(() => repository.fetch(notFoundDataStoreUrl)).rejects.toThrow(DataStoreException)
})

it('throws a DataStoreException when the json is invalid', async () => {
withTempJsonFile('[ invalid }', async (path: string) => {
await expect(() => repository.fetch(`file:${path}`)).rejects.toThrow(DataStoreException)
await withTempJsonFile('[ invalid }', async (path: string) => {
await expect(() => repository.fetch(`file://${path}`)).rejects.toThrow(DataStoreException)
})
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ export class FileSystemDataStoreRepository implements DataStoreRepository {
}

private getPath(url: string): string {
return url.replace('file:', '')
return url.replace('file://', '')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { unlink, writeFile } from 'fs/promises'
import { v4 as uuid } from 'uuid'

/**
* Executes a callback function with a temporary JSON file.
*
* The file is created with the provided data and deleted after the callback is
* executed.
*
* @param data - The data to be written to the temporary JSON file.
* @param thunk - The callback function to be executed with the path of the
* temporary JSON file.
*/
export const withTempJsonFile = async (data: string, thunk: (path: string) => void | Promise<void>) => {
const path = `./test-temp-data-store-${uuid()}.json`

await writeFile(path, data, 'utf-8')

try {
await thunk(path)
} finally {
await unlink(path)
}
}
16 changes: 12 additions & 4 deletions packages/policy-engine-shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,26 @@ export * from './lib/decorators/is-asset-id.decorator'
export * from './lib/decorators/is-hex-string.decorator'
export * from './lib/decorators/is-not-empty-array-enum.decorator'
export * from './lib/decorators/is-not-empty-array-string.decorator'
export * as FIXTURE from './lib/dev.fixture'

export * from './lib/dto'
export * from './lib/schema/address.schema'
export * from './lib/schema/hex.schema'

export * from './lib/type/action.type'
export * from './lib/type/data-store.type'
export * from './lib/type/domain.type'
export * from './lib/type/entity.type'

export * as EntityUtil from './lib/util/entity.util'

export * from './lib/util/caip.util'
export * from './lib/util/encoding.util'
export * as EntityUtil from './lib/util/entity.util'
export * from './lib/util/enum.util'
export * from './lib/util/evm.util'
export * from './lib/util/json.util'
export * from './lib/util/typeguards'

export * from './lib/schema/address.schema'
export * from './lib/schema/data-store.schema'
export * from './lib/schema/entity.schema'
export * from './lib/schema/hex.schema'

export * as FIXTURE from './lib/dev.fixture'
30 changes: 18 additions & 12 deletions packages/policy-engine-shared/src/lib/schema/data-store.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,26 @@ export const dataStoreConfigurationSchema = z.object({
keys: z.array(jsonWebKeySchema)
})

export const entityDataSchema = z.object({
entity: z.object({
data: entitiesSchema
export const entityDataSchema = z
.object({
entity: z.object({
data: entitiesSchema
})
})
})
.describe('Entity data')

export const entitySignatureSchema = z.object({
entity: z.object({
signature: z.string()
export const entitySignatureSchema = z
.object({
entity: z.object({
signature: z.string()
})
})
})
.describe('Entity data signature')

export const entityJsonWebKeySetSchema = z.object({
entity: z.object({
keys: z.array(jsonWebKeySchema)
export const entityJsonWebKeySetSchema = z
.object({
entity: z.object({
keys: z.array(jsonWebKeySchema)
})
})
})
.describe('Entity JWKS')

0 comments on commit a310408

Please sign in to comment.