-
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.
- Loading branch information
0 parents
commit c51f6eb
Showing
11 changed files
with
7,601 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
coverage |
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,2 @@ | ||
*.test.js | ||
fixtures |
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,42 @@ | ||
# Ajv validation for Mali.js gRPC microservices | ||
|
||
This package adds **opinionated** [Ajv](https://ajv.js.org) input validation support for [Mali.js](https://mali.js.org) gRPC microservices. | ||
|
||
## Install | ||
|
||
```bash | ||
npm i --save mali-ajv | ||
``` | ||
|
||
## Setup | ||
|
||
```javascript | ||
import Mali from 'mali' | ||
import { addSchemas } from './grpc.validator.js' | ||
import * as accounts from './endpoints/accounts.schema.js' | ||
import * as memberships from './endpoints/memberships.schema.js' | ||
|
||
const app = new Mali() | ||
app.addService(file, 'Memberships', {}) | ||
app.addService(file, 'Accounts', {}) | ||
app.use(addSchemas(app, { accounts, memberships })) | ||
``` | ||
|
||
## Usage | ||
|
||
Example request: | ||
|
||
```json | ||
{ | ||
"identity_id": "45670d4a-1185-4e5a-bd3", | ||
"name": "" | ||
} | ||
``` | ||
|
||
Throws the following error: | ||
|
||
```json | ||
{ | ||
"error": "9 FAILED_PRECONDITION: data.identity_id should match format \"uuid\"" | ||
} | ||
``` |
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,7 @@ | ||
export const findOne = { | ||
type: 'object', | ||
properties: { | ||
account_id: { type: 'string', format: 'uuid' }, | ||
}, | ||
required: ['account_id'], | ||
} |
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,7 @@ | ||
export const findOne = { | ||
type: 'object', | ||
properties: { | ||
organization_id: { type: 'string', format: 'uuid' }, | ||
}, | ||
required: ['organization_id'], | ||
} |
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,34 @@ | ||
syntax = "proto3"; | ||
|
||
service Accounts { | ||
rpc findOne(AccountFindOne) returns (Account) {} | ||
rpc findAll(Empty) returns (AccountsResponse) {} | ||
} | ||
|
||
service Organizations { | ||
rpc findOne(OrganizationFindOne) returns (Organization) {} | ||
} | ||
|
||
message Empty {} | ||
|
||
message AccountFindOne { | ||
string account_id = 1; | ||
} | ||
|
||
message OrganizationFindOne { | ||
string organization_id = 1; | ||
} | ||
|
||
message Account { | ||
string account_id = 1; | ||
string name = 2; | ||
} | ||
|
||
message Organization { | ||
string organization_id = 1; | ||
string name = 2; | ||
} | ||
|
||
message AccountsResponse { | ||
repeated Account rows = 1; | ||
} |
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,46 @@ | ||
import grpc from '@grpc/grpc-js' | ||
import Ajv from 'ajv' | ||
import uuid from '@socketkit/ajv-uuid' | ||
|
||
const ajv = new Ajv({ removeAdditional: true }) | ||
uuid(ajv) | ||
export default ajv | ||
|
||
/** | ||
* @function addSchemas | ||
* @description Adds JSON schemas to Mali instance | ||
* @param {import('mali').Mali} mali Mali Instance | ||
* @param {object} schemas Schema function declerations | ||
* @returns {Function} callback function | ||
*/ | ||
export function addSchemas(app, schemas = {}) { | ||
app.context.schemas = new Map(Object.entries(schemas)) | ||
|
||
return async function (context, next) { | ||
const { req } = context.request | ||
const name = context.service.toLowerCase() | ||
const service_schemas = app.context.schemas.get(name) | ||
|
||
// Schema does not include the given service decleration | ||
if (!service_schemas) { | ||
return next() | ||
} | ||
|
||
const schema = service_schemas[context.name] | ||
|
||
// Service schema does not include the given function | ||
if (!schema) { | ||
return next() | ||
} | ||
|
||
const isValid = await ajv.validate(schema, req) | ||
|
||
if (!isValid) { | ||
const error = new Error(ajv.errorsText(ajv.errors)) | ||
error.code = grpc.status.FAILED_PRECONDITION | ||
throw error | ||
} | ||
|
||
return next() | ||
} | ||
} |
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,145 @@ | ||
import { randomUUID } from 'crypto' | ||
import path from 'path' | ||
import test from 'ava' | ||
import Mali from 'mali' | ||
import grpc from '@grpc/grpc-js' | ||
import loader from '@grpc/proto-loader' | ||
import { promisify } from 'util' | ||
|
||
import * as account_schemas from './fixtures/accounts.schema.js' | ||
import * as organization_schemas from './fixtures/organizations.schema.js' | ||
|
||
import { addSchemas } from './index.js' | ||
|
||
const options = { | ||
keepCase: true, | ||
longs: String, | ||
enums: String, | ||
defaults: true, | ||
oneofs: true, | ||
} | ||
|
||
const file = path.join(path.resolve('.'), 'fixtures/test-service.proto') | ||
|
||
const getRandomPort = (a = 1000, b = 65000) => { | ||
const lower = Math.ceil(Math.min(a, b)) | ||
const upper = Math.floor(Math.max(a, b)) | ||
return Math.floor(lower + Math.random() * (upper - lower + 1)) | ||
} | ||
|
||
function promisifyAll(subscriber) { | ||
const to = {} | ||
for (const k in subscriber) { | ||
if (typeof subscriber[k] != 'function') continue | ||
to[k] = promisify(subscriber[k].bind(subscriber)) | ||
} | ||
return to | ||
} | ||
|
||
const getClients = (port) => { | ||
const { Accounts, Organizations } = grpc.loadPackageDefinition( | ||
loader.loadSync(path.join('.', 'fixtures/test-service.proto'), options), | ||
) | ||
|
||
return { | ||
accounts: new Accounts(`0.0.0.0:${port}`, grpc.credentials.createInsecure()), | ||
organizations: new Organizations(`0.0.0.0:${port}`, grpc.credentials.createInsecure()), | ||
} | ||
} | ||
|
||
test('should throw error on invalid request', async (t) => { | ||
const port = getRandomPort() | ||
const app = new Mali() | ||
app.addService(file, 'Accounts', options) | ||
app.addService(file, 'Organizations', options) | ||
app.use(addSchemas(app, { accounts: account_schemas })) | ||
app.use('Accounts', 'findOne', (ctx) => { | ||
ctx.res = { account_id: randomUUID(), name: 'hello' } | ||
}) | ||
app.use('Organizations', 'findOne', (ctx) => { | ||
ctx.res = { organization_id: randomUUID(), name: 'hello' } | ||
}) | ||
await app.start(`0.0.0.0:${port}`) | ||
t.teardown(() => app.close()) | ||
|
||
const clients = getClients(port) | ||
const accounts = promisifyAll(clients.accounts) | ||
try { | ||
await accounts.findOne({ account_id: 'hello' }) | ||
throw new Error(`Invalid`) | ||
} catch (error) { | ||
t.not(error.message, 'Invalid') | ||
t.is(error.code, grpc.status.FAILED_PRECONDITION) | ||
t.is(error.details, `data/account_id must match format "uuid"`) | ||
} | ||
}) | ||
|
||
test('should not throw error on valid data', async (t) => { | ||
const port = getRandomPort() | ||
const app = new Mali() | ||
app.addService(file, 'Accounts', options) | ||
app.addService(file, 'Organizations', options) | ||
app.use(addSchemas(app, { accounts: account_schemas })) | ||
app.use('Accounts', 'findOne', (ctx) => { | ||
ctx.res = { account_id: randomUUID(), name: 'hello' } | ||
}) | ||
app.use('Organizations', 'findOne', (ctx) => { | ||
ctx.res = { organization_id: randomUUID(), name: 'hello' } | ||
}) | ||
await app.start(`0.0.0.0:${port}`) | ||
t.teardown(() => app.close()) | ||
|
||
const clients = getClients(port) | ||
const accounts = promisifyAll(clients.accounts) | ||
const response = await accounts.findOne({ account_id: randomUUID() }) | ||
t.truthy(response) | ||
}) | ||
|
||
test('should not throw error on missing schema', async (t) => { | ||
const port = getRandomPort() | ||
const app = new Mali() | ||
app.addService(file, 'Accounts', options) | ||
app.addService(file, 'Organizations', options) | ||
app.use(addSchemas(app, { accounts: account_schemas })) | ||
app.use('Accounts', 'findOne', (ctx) => { | ||
ctx.res = { account_id: randomUUID(), name: 'hello' } | ||
}) | ||
app.use('Organizations', 'findOne', (ctx) => { | ||
ctx.res = { organization_id: randomUUID(), name: 'hello' } | ||
}) | ||
await app.start(`0.0.0.0:${port}`) | ||
t.teardown(() => app.close()) | ||
|
||
const clients = getClients(port) | ||
const organizations = promisifyAll(clients.organizations) | ||
const response = await organizations.findOne({ organization_id: randomUUID() }) | ||
t.truthy(response) | ||
}) | ||
|
||
test('should not throw error on missing function in a schema', async (t) => { | ||
const port = getRandomPort() | ||
const app = new Mali() | ||
app.addService(file, 'Accounts', options) | ||
app.addService(file, 'Organizations', options) | ||
app.use(addSchemas(app, { accounts: account_schemas })) | ||
app.use({ | ||
Accounts: { | ||
findOne: (ctx) => { | ||
ctx.res = { account_id: randomUUID(), name: 'hello' } | ||
}, | ||
findAll: (ctx) => { | ||
ctx.res = { rows: [] } | ||
} | ||
} | ||
}) | ||
app.use('Organizations', 'findOne', (ctx) => { | ||
ctx.res = { organization_id: randomUUID(), name: 'hello' } | ||
}) | ||
await app.start(`0.0.0.0:${port}`) | ||
t.teardown(() => app.close()) | ||
|
||
const clients = getClients(port) | ||
const accounts = promisifyAll(clients.accounts) | ||
const response = await accounts.findAll({}) | ||
t.truthy(response.rows) | ||
}) |
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,12 @@ | ||
{ | ||
"compilerOptions": { | ||
"allowSyntheticDefaultImports": true, | ||
"lib": ["ES2020"], | ||
"module": "ES2020", | ||
"moduleResolution": "node", | ||
"target": "ES2020", | ||
"checkJs": true | ||
}, | ||
"exclude": ["node_modules"], | ||
"include": ["./*.js"] | ||
} |
Oops, something went wrong.