Skip to content

Commit

Permalink
feat: dynamic import, dual module (#149)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: must always call `await validator.init()` prior to first use, no path
to sync usage remains. Furthermore instead of using require() + cache busting via delete
it will now use dynamic import

* feat: dynamic import for schema files, only async init
* feat: dual-module
* chore: increase node version for CI
  • Loading branch information
AVVS authored Jan 3, 2024
1 parent ae413cc commit 73b74ce
Show file tree
Hide file tree
Showing 17 changed files with 658 additions and 3,141 deletions.
File renamed without changes.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ lib
.vscode
.DS_Store

.tsimp
.tshy
.tshy-build
5 changes: 3 additions & 2 deletions .semaphore/semaphore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ global_job_config:
prologue:
commands:
- set -e
- sem-version node 18
- npm i -g pnpm@8
- sem-version node 20.10
- corepack enable
- corepack install --global [email protected]
- checkout
- cache restore node-$(checksum pnpm-lock.yaml)
- pnpm i --frozen-lockfile --prefer-offline
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Based on the bluebird promises.

## Installation

`yarn add @microfleet/validation`
`npm i @microfleet/validation`

## Usage

Expand All @@ -26,6 +26,8 @@ import Errors = require('common-errors');
import Validator, { HttpStatusError } from '@microfleet/validation';
const validator = new Validator('./schemas');

await validator.init()

// some logic here
validator.validate('config', {
configuration: 'string'
Expand Down
3 changes: 3 additions & 0 deletions __tests__/fixtures/cjs/cjs-01.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
"$id": "cjs-01.cjs"
}
1 change: 1 addition & 0 deletions __tests__/fixtures/cjs/cjs-02.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const $id = "cjs-02.cts"
1 change: 1 addition & 0 deletions __tests__/fixtures/cjs/cjs-03.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const $id = "cjs-03.ts"
1 change: 1 addition & 0 deletions __tests__/fixtures/esm/esm-01.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const $id = "esm-01.mjs"
1 change: 1 addition & 0 deletions __tests__/fixtures/esm/esm-02.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const $id = "esm-02.mts"
4 changes: 4 additions & 0 deletions __tests__/fixtures/esm/esm-03-defaults.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
$id: 'esm-03-defaults.mts',
type: 'string'
}
4 changes: 4 additions & 0 deletions __tests__/fixtures/esm/esm-04-defaults.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
$id: 'esm-04-defaults.mjs',
type: 'string'
}
183 changes: 89 additions & 94 deletions __tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import test, { beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import path from 'node:path'
import { io, NotFoundError } from 'common-errors'
import path = require('path');
import Validation, { HttpStatusError } from '../src'
import Validation, { HttpStatusError } from '../src/index'

const CORRECT_PATH = path.resolve(__dirname, './fixtures')
const BAD_PATH = path.resolve(__dirname, './notexistant')
Expand All @@ -15,102 +17,98 @@ beforeEach(() => {
})

test('doesnt init on missing dir', () => {
expect(() => validator.init()).toThrow(TypeError)
assert.rejects(validator.init(), TypeError)
})

test('should successfully init', () => {
validator.init(CORRECT_PATH)
test('should successfully init', async () => {
validator = new Validation(CORRECT_PATH, () => true)
await validator.init()

expect(typeof validator.ajv.getSchema('custom')).toBe('function')
expect(typeof validator.ajv.getSchema('core-no-id')).toBe('function')
expect(typeof validator.ajv.getSchema('nested.no-id')).toBe('function')
assert.equal(typeof validator.ajv.getSchema('custom'), 'function', 'custom')
assert.equal(typeof validator.ajv.getSchema('core-no-id'), 'function', 'core-no-id')
assert.equal(typeof validator.ajv.getSchema('nested.no-id'), 'function')

assert.equal(typeof validator.ajv.getSchema('cjs-01.cjs'), 'function', 'cjs-01.cjs')
assert.equal(typeof validator.ajv.getSchema('cjs-02.cts'), 'function', 'cjs-02.cts')
assert.equal(typeof validator.ajv.getSchema('cjs-03.ts'), 'function', 'cjs-03.ts')

assert.equal(typeof validator.ajv.getSchema('esm-01.mjs'), 'function', 'esm-01.mjs')
assert.equal(typeof validator.ajv.getSchema('esm-02.mts'), 'function', 'esm-02.mts')
assert.equal(typeof validator.ajv.getSchema('esm-03-defaults.mts'), 'function', 'esm-03-defaults.mts')
assert.equal(typeof validator.ajv.getSchema('esm-04-defaults.mjs'), 'function', 'esm-04-defaults.mjs')
})

test('should successfully init with a relative path', () => {
validator.init(RELATIVE_PATH)
test('should successfully init with a relative path', async () => {
await validator.init(RELATIVE_PATH)

expect(typeof validator.ajv.getSchema('custom')).toBe('function')
expect(typeof validator.ajv.getSchema('core-no-id')).toBe('function')
expect(typeof validator.ajv.getSchema('nested.no-id')).toBe('function')
assert.equal(typeof validator.ajv.getSchema('custom'), 'function')
assert.equal(typeof validator.ajv.getSchema('core-no-id'), 'function')
assert.equal(typeof validator.ajv.getSchema('nested.no-id'), 'function')
})

test('(async) should successfully init', async () => {
await validator.init(CORRECT_PATH, true)
await validator.init(CORRECT_PATH)

expect(typeof validator.ajv.getSchema('custom')).toBe('function')
expect(typeof validator.ajv.getSchema('core-no-id')).toBe('function')
expect(typeof validator.ajv.getSchema('nested.no-id')).toBe('function')
assert.equal(typeof validator.ajv.getSchema('custom'), 'function')
assert.equal(typeof validator.ajv.getSchema('core-no-id'), 'function')
assert.equal(typeof validator.ajv.getSchema('nested.no-id'), 'function')
})

test('(async) should successfully init with a relative path', async () => {
await validator.init(RELATIVE_PATH, true)
await validator.init(RELATIVE_PATH)

expect(typeof validator.ajv.getSchema('custom')).toBe('function')
expect(typeof validator.ajv.getSchema('core-no-id')).toBe('function')
expect(typeof validator.ajv.getSchema('nested.no-id')).toBe('function')
assert.equal(typeof validator.ajv.getSchema('custom'), 'function')
assert.equal(typeof validator.ajv.getSchema('core-no-id'), 'function')
assert.equal(typeof validator.ajv.getSchema('nested.no-id'), 'function')
})

test('should reject promise with an IO Error on invalid dir', async () => {
expect.assertions(1)
await expect(validator.init(BAD_PATH, true))
.rejects.toThrow(io.IOError)
await assert.rejects(validator.init(BAD_PATH), io.IOError)
})

test('should reject promise with a file not found error on an empty dir', async () => {
expect.assertions(1)
await expect(validator.init(EMPTY_PATH, true))
.rejects.toThrow(io.FileNotFoundError)
await assert.rejects(validator.init(EMPTY_PATH), io.FileNotFoundError)
})

test('should reject promise with a io error on a non-dir', async () => {
expect.assertions(1)
await expect(validator.init(FILE_PATH, true))
.rejects.toThrow(io.IOError)
await assert.rejects(validator.init(FILE_PATH), io.IOError)
})

test('should reject with a io error on a non-dir', async () => {
expect.assertions(1)
expect(() => validator.init(FILE_PATH)).toThrow(io.IOError)
assert.rejects(validator.init(FILE_PATH), io.IOError)
})

test('should reject promise with a NotFoundError on a non-existant validator', async () => {
expect.assertions(1)
validator.init(CORRECT_PATH)
await expect(validator.validate('bad-route', {}))
.rejects.toThrow(NotFoundError)
await validator.init(CORRECT_PATH)
await assert.rejects(validator.validate('bad-route', {}), NotFoundError)
})

test('should validate a correct object', async () => {
expect.assertions(1)
validator.init(CORRECT_PATH)
await expect(validator.validate('custom', { string: 'not empty' }))
.resolves.toEqual({ string: 'not empty' })
await validator.init(CORRECT_PATH)
assert.deepEqual(await validator.validate('custom', { string: 'not empty' }), { string: 'not empty' })
})

test('should filter extra properties', async () => {
expect.assertions(1)
validator = new Validation(CORRECT_PATH, null, { removeAdditional: true })
await expect(validator.filter('custom', { string: 'not empty', qq: 'not in schema' }))
.resolves.toEqual({ string: 'not empty' })
await validator.init()
assert.deepEqual(await validator.filter('custom', { string: 'not empty', qq: 'not in schema' }), { string: 'not empty' })
})

test('should filter extra properties, but still throw on invalid data', async () => {
expect.assertions(1)
validator = new Validation(CORRECT_PATH, null, { removeAdditional: true })
await expect(validator.filter('custom', { string: 20, qq: 'not in schema' }))
.rejects.toThrow(HttpStatusError)
await validator.init()
await assert.rejects(validator.filter('custom', { string: 20, qq: 'not in schema' }), HttpStatusError)
})

test('should return validation error on an invalid object', async () => {
const reject = async (): Promise<any> => validator.validate('custom', { string: 'not empty', extraneous: true })

expect.assertions(3)
validator.init(CORRECT_PATH)
await expect(reject()).rejects.toThrow(HttpStatusError)
await validator.init(CORRECT_PATH)
await assert.rejects(reject(), HttpStatusError)

const error = await reject().catch((e) => e)
expect(error.statusCode).toBe(417)
expect(JSON.parse(JSON.stringify(error))).toEqual({
assert.equal(error.statusCode, 417)
assert.deepEqual(JSON.parse(JSON.stringify(error)), {
errors: [{
field: '/extraneous',
message: 'must NOT have additional properties',
Expand All @@ -127,71 +125,68 @@ test('should return validation error on an invalid object', async () => {
})
})

test('should perform sync validation', () => {
validator.init(CORRECT_PATH)
test('should perform sync validation', async () => {
await validator.init(CORRECT_PATH)
const result = validator.validateSync('custom', { string: 'not empty' })
expect(result.error).toBeNull()
expect(result.doc).toEqual({ string: 'not empty' })
assert.equal(result.error, null)
assert.deepEqual(result.doc, { string: 'not empty' })
})

test('should filter out extra props on sync validation', () => {
test('should filter out extra props on sync validation', async () => {
validator = new Validation(CORRECT_PATH, null, { removeAdditional: true })
await validator.init()

const result = validator.validateSync('custom', { string: 'not empty', extra: true })
// ajv does not throw errors in this case
expect(result.error).toBeNull()
expect(result.doc).toEqual({ string: 'not empty' })
assert.equal(result.error, null)
assert.deepEqual(result.doc, { string: 'not empty' })
})

test('throws when using ifError', () => {
test('throws when using ifError', async () => {
validator = new Validation(CORRECT_PATH, null, { removeAdditional: true })
expect(() => validator.ifError('custom', { string: 200, extra: true }))
.toThrow(HttpStatusError)
await validator.init()
assert.throws(() => validator.ifError('custom', { string: 200, extra: true }), HttpStatusError)
})

test('doesn\'t throw on ifError', () => {
test('doesn\'t throw on ifError', async () => {
validator = new Validation(CORRECT_PATH, null, { removeAdditional: true })
expect(validator.ifError('custom', { string: 'not empty', extra: true })).toEqual({
await validator.init()
assert.deepEqual(validator.ifError('custom', { string: 'not empty', extra: true }), {
string: 'not empty',
})
})

test('validates ReDos prone URL', () => {
test('validates ReDos prone URL', async () => {
validator = new Validation(CORRECT_PATH, null, { removeAdditional: false })
expect(validator.ifError<string>('http-url', 'https://google.com12349834543489525824485'))
.toEqual('https://google.com12349834543489525824485')
await validator.init()
assert.equal(
validator.ifError<string>('http-url', 'https://google.com12349834543489525824485'),
'https://google.com12349834543489525824485')
})

test('throws on invalid URL', () => {
test('throws on invalid URL', async () => {
validator = new Validation(CORRECT_PATH, null, { removeAdditional: false })
expect(() => validator.ifError<string>('http-url', 'ftp://crap'))
.toThrow(HttpStatusError)
expect(() => validator.ifError<string>('http-url', 'https://super.duper:8443'))
.toThrow(HttpStatusError)
expect(() => validator.ifError<string>('http-url', 'https://'))
.toThrow(HttpStatusError)
expect(() => validator.ifError<string>('http-url', 'http://notld'))
.toThrow(HttpStatusError)
expect(() => validator.ifError<string>('http-url', 'http://notld:8443'))
.toThrow(HttpStatusError)
expect(() => validator.ifError<string>('http-url', 'http://notld. :8443'))
.toThrow(HttpStatusError)
expect(() => validator.ifError<string>('http-url', 'http://notld.'))
.toThrow(HttpStatusError)
expect(() => validator.ifError<string>('http-url', 'http://notld. '))
.toThrow(HttpStatusError)
})

test('do not throw on valid URL', () => {
await validator.init()

assert.throws(() => validator.ifError<string>('http-url', 'ftp://crap'), 'HttpStatusError')
assert.throws(() => validator.ifError<string>('http-url', 'https://super.duper:8443'), 'HttpStatusError')
assert.throws(() => validator.ifError<string>('http-url', 'https://'), 'HttpStatusError')
assert.throws(() => validator.ifError<string>('http-url', 'http://notld'), 'HttpStatusError')
assert.throws(() => validator.ifError<string>('http-url', 'http://notld:8443'), 'HttpStatusError')
assert.throws(() => validator.ifError<string>('http-url', 'http://notld. :8443'), 'HttpStatusError')
assert.throws(() => validator.ifError<string>('http-url', 'http://notld.'), 'HttpStatusError')
assert.throws(() => validator.ifError<string>('http-url', 'http://notld. '), 'HttpStatusError')
})

test('do not throw on valid URL', async () => {
validator = new Validation(CORRECT_PATH, null, { removeAdditional: false })
expect(validator.ifError<string>('http-url', 'https://super.duper#hash'))
.toEqual('https://super.duper#hash')
await validator.init()
assert.equal(validator.ifError<string>('http-url', 'https://super.duper#hash'), 'https://super.duper#hash')
})

test('should be able to use 2019-09 schema keywords', () => {
test('should be able to use 2019-09 schema keywords', async () => {
validator = new Validation(CORRECT_PATH)

expect(() => validator.ifError<number[]>('2019-09', [1]))
.toThrow(HttpStatusError)
expect(validator.ifError<number[]>('2019-09', [1, 2]))
.toStrictEqual([1, 2])
await validator.init()
assert.throws(() => validator.ifError<number[]>('2019-09', [1]), 'HttpStatusError')
assert.deepEqual(validator.ifError<number[]>('2019-09', [1, 2]), [1, 2])
})
13 changes: 0 additions & 13 deletions jest.config.js

This file was deleted.

Loading

0 comments on commit 73b74ce

Please sign in to comment.