Skip to content

Commit

Permalink
feat: add support for async based resource operation handlers (#523)
Browse files Browse the repository at this point in the history
  • Loading branch information
pablopalacios authored Aug 20, 2024
1 parent 20917bb commit 1ea1cad
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 285 deletions.
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ export default {
// resource is required
resource: 'data_service',
// at least one of the CRUD methods is required
read: function (req, resource, params, config, callback) {
//...
read: async function ({ req, resource, params, config }) {
return { data: 'foo' };
},
// other methods
// create: function(req, resource, params, body, config, callback) {},
// update: function(req, resource, params, body, config, callback) {},
// delete: function(req, resource, params, config, callback) {}
// create: async function({ req, resource, params, body, config }) {},
// update: async function({ req, resource, params, body, config }) {},
// delete: async function({ req, resource, params, config }) {}
};
```

Expand Down Expand Up @@ -173,16 +173,16 @@ property of the resolved value.
// dataService.js
export default {
resource: 'data_service',
read: function (req, resource, params, config, callback) {
// business logic
const data = 'response';
const meta = {
headers: {
'cache-control': 'public, max-age=3600',
read: async function ({ req, resource, params, config }) {
return {
data: 'response', // business logic
meta: {
headers: {
'cache-control': 'public, max-age=3600',
},
statusCode: 200, // You can even provide a custom statusCode for the fetch response
},
statusCode: 200, // You can even provide a custom statusCode for the fetch response
};
callback(null, data, meta);
},
};
```
Expand Down Expand Up @@ -225,17 +225,17 @@ fetcher.updateOptions({

## Error Handling

When an error occurs in your Fetchr CRUD method, you should return an error object to the callback. The error object should contain a `statusCode` (default 500) and `output` property that contains a JSON serializable object which will be sent to the client.
When an error occurs in your Fetchr CRUD method, you should throw an error object. The error object should contain a `statusCode` (default 500) and `output` property that contains a JSON serializable object which will be sent to the client.

```js
export default {
resource: 'FooService',
read: function create(req, resource, params, configs, callback) {
read: async function create(req, resource, params, configs) {
const err = new Error('it failed');
err.statusCode = 404;
err.output = { message: 'Not found', more: 'meta data' };
err.meta = { foo: 'bar' };
return callback(err);
throw err;
},
};
```
Expand Down
174 changes: 101 additions & 73 deletions libs/fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const OP_READ = 'read';
const OP_CREATE = 'create';
const OP_UPDATE = 'update';
const OP_DELETE = 'delete';
const OPERATIONS = [OP_READ, OP_CREATE, OP_UPDATE, OP_DELETE];

const RESOURCE_SANTIZER_REGEXP = /[^\w.]+/g;

class FetchrError extends Error {
Expand All @@ -18,6 +20,21 @@ class FetchrError extends Error {
}
}

function _checkResourceHandlers(service) {
for (const operation of OPERATIONS) {
const handler = service[operation];
if (!handler) {
continue;
}

if (handler.length > 1) {
console.warn(
`${service.resource} ${operation} handler is callback based. Callback based resource handlers are deprecated and will be removed in the next version.`,
);
}
}
}

function parseValue(value) {
// take care of value of type: array, object
try {
Expand Down Expand Up @@ -200,26 +217,83 @@ class Request {
* @param {Object} errData The error response for failed request
* @param {Object} result The response data for successful request
*/
_captureMetaAndStats(errData, result) {
const meta = (errData && errData.meta) || (result && result.meta);
_captureMetaAndStats(err, meta) {
if (meta) {
this.serviceMeta.push(meta);
}
if (typeof this._statsCollector === 'function') {
const err = errData && errData.err;
this._statsCollector({
resource: this.resource,
operation: this.operation,
params: this._params,
statusCode: err
? err.statusCode
: (result && result.meta && result.meta.statusCode) || 200,
: (meta && meta.statusCode) || 200,
err,
time: Date.now() - this._startTime,
});
}
}

/**
* Execute this fetcher request
* @returns {Promise<FetchrResponse>}
*/
async _executeRequest() {
if (!Fetcher.isRegistered(this.resource)) {
const err = new FetchrError(
`Service "${sanitizeResourceName(this.resource)}" could not be found`,
);
return { err };
}

const service = Fetcher.getService(this.resource);
const handler = service[this.operation];

if (!handler) {
const err = new FetchrError(
`operation: ${this.operation} is undefined on service: ${this.resource}`,
);
return { err };
}

// async based handler
if (handler.length <= 1) {
return handler
.call(service, {
body: this._body,
config: this._clientConfig,
params: this._params,
req: this.req,
resource: this.resource,
})
.catch((err) => ({ err }));
}

// callback based handler
return new Promise((resolve) => {
const args = [
this.req,
this.resource,
this._params,
this._clientConfig,
function executeRequestCallback(err, data, meta) {
resolve({ err, data, meta });
},
];

if (this.operation === OP_CREATE || this.operation === OP_UPDATE) {
args.splice(3, 0, this._body);
}

try {
handler.apply(service, args);
} catch (err) {
resolve({ err });
}
});
}

/**
* Execute this fetcher request and call callback.
* @param {fetcherCallback} callback callback invoked when service
Expand All @@ -234,31 +308,19 @@ class Request {

this._startTime = Date.now();

const promise = new Promise((resolve, reject) => {
setImmediate(executeRequest, this, resolve, reject);
}).then(
(result) => {
this._captureMetaAndStats(null, result);
return result;
},
(errData) => {
this._captureMetaAndStats(errData);
throw errData.err;
},
);

if (callback) {
promise.then(
(result) => {
setImmediate(callback, null, result.data, result.meta);
},
(err) => {
setImmediate(callback, err);
},
);
} else {
return promise;
}
return this._executeRequest().then(({ err, data, meta }) => {
this._captureMetaAndStats(err, meta);
if (callback) {
callback(err, data, meta);
return;
}

if (err) {
throw err;
}

return { data, meta };
});
}

then(resolve, reject) {
Expand All @@ -280,45 +342,6 @@ class Request {
}
}

/**
* Execute and resolve/reject this fetcher request
* @param {Object} request Request instance object
* @param {Function} resolve function to call when request fulfilled
* @param {Function} reject function to call when request rejected
*/
function executeRequest(request, resolve, reject) {
const args = [
request.req,
request.resource,
request._params,
request._clientConfig,
function executeRequestCallback(err, data, meta) {
if (err) {
reject({ err, meta });
} else {
resolve({ data, meta });
}
},
];

const op = request.operation;
if (op === OP_CREATE || op === OP_UPDATE) {
args.splice(3, 0, request._body);
}

try {
const service = Fetcher.getService(request.resource);
if (!service[op]) {
throw new FetchrError(
`operation: ${op} is undefined on service: ${request.resource}`,
);
}
service[op].apply(service, args);
} catch (err) {
reject({ err });
}
}

class Fetcher {
/**
* Fetcher class for the server.
Expand Down Expand Up @@ -386,6 +409,7 @@ class Fetcher {
'"resource" property is missing in service definition.',
);
}
_checkResourceHandlers(service);

Fetcher.services[resource] = service;
return;
Expand Down Expand Up @@ -485,18 +509,14 @@ following services definitions: ${services}.`);
return next(badOperationError(resource, operation));
}

const serviceMeta = [];

new Request(operation, resource, {
req,
serviceMeta,
statsCollector,
paramsProcessor,
})
.params(params)
.body(body)
.end((err, data) => {
const meta = serviceMeta[0] || {};
.end((err, data, meta = {}) => {
if (meta.headers) {
res.set(meta.headers);
}
Expand Down Expand Up @@ -694,3 +714,11 @@ module.exports = Fetcher;
* @param {Object} [meta] request meta-data
* @param {number} [meta.statusCode=200] http status code to return
*/

/**
* @typedef {Object} FetchrResponse
* @property {?object} data - Any data returned by the fetchr resource.
* @property {?object} meta - Any meta data returned by the fetchr resource.
* @property {?Error} err - an error that occurred before, during or
* after request was sent.
*/
16 changes: 8 additions & 8 deletions tests/functional/resources/alwaysSlow.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
const wait = require('./wait');

// This resource allows us to exercise timeout and abort capacities of
// the fetchr client.

const alwaysSlowService = {
resource: 'slow',
read(req, resource, params, config, callback) {
setTimeout(() => {
callback(null, { ok: true });
}, 5000);
async read() {
await wait(5000);
return { data: { ok: true } };
},
create(req, resource, params, body, config, callback) {
setTimeout(() => {
callback(null, { ok: true });
}, 5000);
async create() {
await wait(5000);
return { data: { ok: true } };
},
};

Expand Down
30 changes: 18 additions & 12 deletions tests/functional/resources/error.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
const wait = require('./wait');

const retryToggle = { error: true };

const errorsService = {
resource: 'error',
read(req, resource, params, config, callback) {
async read({ params }) {
if (params.error === 'unexpected') {
throw new Error('unexpected');
}

if (params.error === 'timeout') {
setTimeout(() => {
callback(null, { ok: true });
}, 100);
return;
await wait(100);
return { data: { ok: true } };
}

if (params.error === 'retry') {
if (retryToggle.error) {
retryToggle.error = false;
const err = new Error('retry');
err.statusCode = 408;
callback(err);
} else {
callback(null, { retry: 'ok' });
throw err;
}
return;

return { data: { retry: 'ok' } };
}

const err = new Error('error');
err.statusCode = 400;
callback(err, null, { foo: 'bar' });

return {
err,
meta: {
foo: 'bar',
},
};
},
create(req, resource, params, body, config, callback) {
this.read(req, resource, params, config, callback);

async create({ params }) {
return this.read({ params });
},
};

Expand Down
Loading

0 comments on commit 1ea1cad

Please sign in to comment.