Skip to content

Commit

Permalink
fix(hooks): Add Deno tests and build, CI and fix build (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
daffl committed Mar 31, 2021
1 parent c236087 commit 44787cd
Show file tree
Hide file tree
Showing 14 changed files with 621 additions and 22 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/deno.yml
@@ -0,0 +1,26 @@
name: Deno CI

on: [push, pull_request]

jobs:
test:
runs-on: "ubuntu-latest"
steps:
- name: Setup repo
uses: actions/checkout@v2

- name: Setup Deno
uses: denolib/setup-deno@v2
with:
deno-version: v1.x

- name: Install Node.js 14
uses: actions/setup-node@v1
with:
node-version: 14

- name: Install build tooling
run: npm install

- name: Run Tests
run: npm run test:deno
5 changes: 3 additions & 2 deletions .github/workflows/nodejs.yml
@@ -1,3 +1,4 @@

name: CI

on: [push, pull_request]
Expand All @@ -9,7 +10,7 @@ jobs:

strategy:
matrix:
node-version: [10.x, 12.x, 13.x]
node-version: [13.x, 15.x]

steps:
- uses: actions/checkout@v2
Expand All @@ -21,4 +22,4 @@ jobs:
- run: npm install
- run: npm test
env:
CI: true
CI: true
1 change: 0 additions & 1 deletion .gitignore
Expand Up @@ -62,6 +62,5 @@ typings/

# Build folders
lib/
deno/
dist/
*.sqlite
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -5,7 +5,8 @@
"install": "lerna bootstrap",
"publish": "lerna publish && git push origin master",
"lint": "tslint 'packages/**/src/*.ts' 'packages/**/test/*.ts' -c tslint.json --fix",
"test": "npm run lint && nyc lerna run test"
"test": "npm run lint && nyc lerna run test",
"test:deno": "lerna run test:deno"
},
"devDependencies": {
"lerna": "^4.0.0",
Expand Down
6 changes: 2 additions & 4 deletions packages/hooks/build/deno.js
Expand Up @@ -3,10 +3,8 @@
const path = require('path');
const fs = require('fs');

const moduleNames = [
'./base', './compose', './decorator',
'./function', './object'
];
const moduleNames = fs.readdirSync(path.join(__dirname, '..', 'src'))
.map(mod => `./${mod.replace('.ts', '')}`);

const folder = path.join(__dirname, '..', 'src');
const out = path.join(__dirname, '..', 'deno');
Expand Down
210 changes: 210 additions & 0 deletions packages/hooks/deno/base.ts
@@ -0,0 +1,210 @@
import { Middleware } from './compose.ts';
import { copyToSelf, copyProperties } from './utils.ts';

export const HOOKS: string = Symbol('@feathersjs/hooks') as any;

export type HookContextData = { [key: string]: any };

/**
* The base hook context.
*/
export class HookContext<T = any, C = any> {
result?: T;
method?: string;
self: C;
arguments: any[];
[key: string]: any;

constructor (data: HookContextData = {}) {
Object.assign(this, data);
}
}

export type HookContextConstructor = new (data?: { [key: string]: any }) => HookContext;

export type HookDefaultsInitializer = (self?: any, args?: any[], context?: HookContext) => HookContextData;

export class HookManager {
_parent?: this|null = null;
_params: string[]|null = null;
_middleware: Middleware[]|null = null;
_props: HookContextData|null = null;
_defaults: HookDefaultsInitializer;

parent (parent: this) {
this._parent = parent;

return this;
}

middleware (middleware?: Middleware[]) {
this._middleware = middleware?.length ? middleware : null;

return this;
}

getMiddleware (): Middleware[]|null {
const previous = this._parent?.getMiddleware();

if (previous && this._middleware) {
return previous.concat(this._middleware);
}

return previous || this._middleware;
}

collectMiddleware (self: any, _args: any[]): Middleware[] {
const otherMiddleware = getMiddleware(self);
const middleware = this.getMiddleware();

if (otherMiddleware && middleware) {
return otherMiddleware.concat(middleware);
}

return otherMiddleware || middleware;
}

props (props: HookContextData) {
if (!this._props) {
this._props = {};
}

copyProperties(this._props, props);

return this;
}

getProps (): HookContextData {
const previous = this._parent?.getProps();

if (previous && this._props) {
return copyProperties({}, previous, this._props);
}

return previous || this._props;
}

params (...params: string[]) {
this._params = params;

return this;
}

getParams (): string[] {
const previous = this._parent?.getParams();

if (previous && this._params) {
return previous.concat(this._params);
}

return previous || this._params;
}

defaults (defaults: HookDefaultsInitializer) {
this._defaults = defaults;

return this;
}

getDefaults (self: any, args: any[], context: HookContext): HookContextData {
const defaults = typeof this._defaults === 'function' ? this._defaults(self, args, context) : null;
const previous = this._parent?.getDefaults(self, args, context);

if (previous && defaults) {
return Object.assign({}, previous, defaults);
}

return previous || defaults;
}

getContextClass (Base: HookContextConstructor = HookContext): HookContextConstructor {
const ContextClass = class ContextClass extends Base {
constructor (data: any) {
super(data);

copyToSelf(this);
}
};
const params = this.getParams();
const props = this.getProps();

if (params) {
params.forEach((name, index) => {
if (props?.[name] !== undefined) {
throw new Error(`Hooks can not have a property and param named '${name}'. Use .defaults instead.`);
}

Object.defineProperty(ContextClass.prototype, name, {
enumerable: true,
get () {
return this?.arguments[index];
},
set (value: any) {
this.arguments[index] = value;
}
});
});
}

if (props) {
copyProperties(ContextClass.prototype, props);
}

return ContextClass;
}

initializeContext (self: any, args: any[], context: HookContext): HookContext {
const ctx = this._parent ? this._parent.initializeContext(self, args, context) : context;
const defaults = this.getDefaults(self, args, ctx);

if (self) {
ctx.self = self;
}

ctx.arguments = args;

if (defaults) {
for (const name of Object.keys(defaults)) {
if (ctx[name] === undefined) {
ctx[name] = defaults[name];
}
}
}

return ctx;
}
}

export type HookOptions = HookManager|Middleware[]|null;

export function convertOptions (options: HookOptions = null) {
if (!options) {
return new HookManager()
}

return Array.isArray(options) ? new HookManager().middleware(options) : options;
}

export function getManager (target: any): HookManager|null {
return (target && target[HOOKS]) || null;
}

export function setManager<T> (target: T, manager: HookManager) {
const parent = getManager(target);

(target as any)[HOOKS] = manager.parent(parent);

return target;
}

export function getMiddleware (target: any): Middleware[]|null {
const manager = getManager(target);

return manager ? manager.getMiddleware() : null;
}

export function setMiddleware<T> (target: T, middleware: Middleware[]) {
const manager = new HookManager().middleware(middleware);

return setManager(target, manager);
}
47 changes: 47 additions & 0 deletions packages/hooks/deno/compose.ts
@@ -0,0 +1,47 @@
// TypeScript port of koa-compose (https://github.com/koajs/compose)
export type NextFunction = () => Promise<any>;

export type Middleware<T = any> = (context: T, next: NextFunction) => Promise<any>;

export function compose<T = any> (middleware: Middleware<T>[]) {
if (!Array.isArray(middleware)) {
throw new TypeError('Middleware stack must be an array!');
}

for (const fn of middleware) {
if (typeof fn !== 'function') {
throw new TypeError('Middleware must be composed of functions!');
}
}

return function (this: any, context: T, next?: Middleware<T>) {
// last called middleware #
let index: number = -1;

return dispatch.call(this, 0);

function dispatch (this: any, i: number): Promise<any> {
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'));
}

index = i;

let fn = middleware[i];

if (i === middleware.length) {
fn = next;
}

if (!fn) {
return Promise.resolve();
}

try {
return Promise.resolve(fn.call(this, context, dispatch.bind(this, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}

0 comments on commit 44787cd

Please sign in to comment.