Skip to content

Commit

Permalink
Merge pull request #19 from scimmyjs/feature/6-gress-request-context
Browse files Browse the repository at this point in the history
Add request context to ingress/egress/degress handlers
  • Loading branch information
sleelin authored Apr 9, 2024
2 parents d28cbbf + b36060d commit c384c22
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 37 deletions.
21 changes: 10 additions & 11 deletions src/lib/resources/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ export class Group extends Types.Resource {
* // Retrieve groups with a group name starting with "A"
* await (new SCIMMY.Resources.Group({filter: 'displayName -sw "A"'})).read();
*/
async read() {
async read(ctx) {
if (!this.id) {
return new Messages.ListResponse((await Group.#egress(this) ?? [])
return new Messages.ListResponse((await Group.#egress(this, ctx) ?? [])
.map(u => new Schemas.Group(u, "out", Group.basepath(), this.attributes)), this.constraints);
} else {
try {
return new Schemas.Group((await Group.#egress(this) ?? []).shift(), "out", Group.basepath(), this.attributes);
return new Schemas.Group([await Group.#egress(this, ctx)].flat().shift(), "out", Group.basepath(), this.attributes);
} catch (ex) {
if (ex instanceof Types.Error) throw ex;
else if (ex instanceof TypeError) throw new Types.Error(400, "invalidValue", ex.message);
Expand All @@ -97,16 +97,15 @@ export class Group extends Types.Resource {
* // Set members attribute for group with ID "1234"
* await (new SCIMMY.Resources.Group("1234")).write({members: [{value: "5678"}]});
*/
async write(instance) {
async write(instance, ctx) {
if (instance === undefined)
throw new Types.Error(400, "invalidSyntax", `Missing request body payload for ${!!this.id ? "PUT" : "POST"} operation`);
if (Object(instance) !== instance || Array.isArray(instance))
throw new Types.Error(400, "invalidSyntax", `Operation ${!!this.id ? "PUT" : "POST"} expected request body payload to be single complex value`);

try {
// TODO: handle incoming read-only and immutable attribute tests
return new Schemas.Group(
await Group.#ingress(this, new Schemas.Group(instance, "in")),
await Group.#ingress(this, new Schemas.Group(instance, "in"), ctx),
"out", Group.basepath(), this.attributes
);
} catch (ex) {
Expand All @@ -124,16 +123,16 @@ export class Group extends Types.Resource {
* // Add member to group with ID "1234" with a patch operation (see SCIMMY.Messages.PatchOp)
* await (new SCIMMY.Resources.Group("1234")).patch({Operations: [{op: "add", path: "members", value: {value: "5678"}}]});
*/
async patch(message) {
async patch(message, ctx) {
if (message === undefined)
throw new Types.Error(400, "invalidSyntax", "Missing message body from PatchOp request");
if (Object(message) !== message || Array.isArray(message))
throw new Types.Error(400, "invalidSyntax", "PatchOp request expected message body to be single complex value");

try {
return await Promise.resolve(new Messages.PatchOp(message)
.apply(new Schemas.Group((await Group.#egress(this) ?? []).shift()),
async (instance) => await Group.#ingress(this, instance)))
.apply(new Schemas.Group([await Group.#egress(this, ctx)].flat().shift()),
async (instance) => await Group.#ingress(this, instance, ctx)))
.then(instance => !instance ? undefined : new Schemas.Group(instance, "out", Group.basepath(), this.attributes));
} catch (ex) {
if (ex instanceof Types.Error) throw ex;
Expand All @@ -148,12 +147,12 @@ export class Group extends Types.Resource {
* // Delete group with ID "1234"
* await (new SCIMMY.Resources.Group("1234")).dispose();
*/
async dispose() {
async dispose(ctx) {
if (!this.id)
throw new Types.Error(404, null, "DELETE operation must target a specific resource");

try {
await Group.#degress(this);
await Group.#degress(this, ctx);
} catch (ex) {
if (ex instanceof Types.Error) throw ex;
else if (ex instanceof TypeError) throw new Types.Error(500, null, ex.message);
Expand Down
20 changes: 10 additions & 10 deletions src/lib/resources/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ export class User extends Types.Resource {
* // Retrieve users with an email ending in "@example.com"
* await (new SCIMMY.Resources.User({filter: 'email.value -ew "@example.com"'})).read();
*/
async read() {
async read(ctx) {
if (!this.id) {
return new Messages.ListResponse((await User.#egress(this) ?? [])
return new Messages.ListResponse((await User.#egress(this, ctx) ?? [])
.map(u => new Schemas.User(u, "out", User.basepath(), this.attributes)), this.constraints);
} else {
try {
return new Schemas.User((await User.#egress(this) ?? []).shift(), "out", User.basepath(), this.attributes);
return new Schemas.User([await User.#egress(this, ctx)].flat().shift(), "out", User.basepath(), this.attributes);
} catch (ex) {
if (ex instanceof Types.Error) throw ex;
else if (ex instanceof TypeError) throw new Types.Error(400, "invalidValue", ex.message);
Expand All @@ -97,7 +97,7 @@ export class User extends Types.Resource {
* // Set userName attribute to "someGuy" for user with ID "1234"
* await (new SCIMMY.Resources.User("1234")).write({userName: "someGuy"});
*/
async write(instance) {
async write(instance, ctx) {
if (instance === undefined)
throw new Types.Error(400, "invalidSyntax", `Missing request body payload for ${!!this.id ? "PUT" : "POST"} operation`);
if (Object(instance) !== instance || Array.isArray(instance))
Expand All @@ -106,7 +106,7 @@ export class User extends Types.Resource {
try {
// TODO: handle incoming read-only and immutable attribute tests
return new Schemas.User(
await User.#ingress(this, new Schemas.User(instance, "in")),
await User.#ingress(this, new Schemas.User(instance, "in"), ctx),
"out", User.basepath(), this.attributes
);
} catch (ex) {
Expand All @@ -124,16 +124,16 @@ export class User extends Types.Resource {
* // Set userName to "someGuy" for user with ID "1234" with a patch operation (see SCIMMY.Messages.PatchOp)
* await (new SCIMMY.Resources.User("1234")).patch({Operations: [{op: "add", value: {userName: "someGuy"}}]});
*/
async patch(message) {
async patch(message, ctx) {
if (message === undefined)
throw new Types.Error(400, "invalidSyntax", "Missing message body from PatchOp request");
if (Object(message) !== message || Array.isArray(message))
throw new Types.Error(400, "invalidSyntax", "PatchOp request expected message body to be single complex value");

try {
return await Promise.resolve(new Messages.PatchOp(message)
.apply(new Schemas.User((await User.#egress(this) ?? []).shift()),
async (instance) => await User.#ingress(this, instance)))
.apply(new Schemas.User([await User.#egress(this, ctx)].flat().shift()),
async (instance) => await User.#ingress(this, instance, ctx)))
.then(instance => !instance ? undefined : new Schemas.User(instance, "out", User.basepath(), this.attributes));
} catch (ex) {
if (ex instanceof Types.Error) throw ex;
Expand All @@ -148,12 +148,12 @@ export class User extends Types.Resource {
* // Delete user with ID "1234"
* await (new SCIMMY.Resources.User("1234")).dispose();
*/
async dispose() {
async dispose(ctx) {
if (!this.id)
throw new Types.Error(404, null, "DELETE operation must target a specific resource");

try {
await User.#degress(this);
await User.#degress(this, ctx);
} catch (ex) {
if (ex instanceof Types.Error) throw ex;
else if (ex instanceof TypeError) throw new Types.Error(500, null, ex.message);
Expand Down
5 changes: 4 additions & 1 deletion src/lib/types/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,10 @@ const isoDate = /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12
* If SCIMMY's filter expression resource matching does not meet your needs, it can be substituted for another implementation
* (e.g. [scim2-parse-filter](https://github.com/thomaspoignant/scim2-parse-filter)) when filtering results within your implementation
* of each resource type's {@link SCIMMY.Types.Resource.ingress|ingress}/{@link SCIMMY.Types.Resource.egress|egress}/{@link SCIMMY.Types.Resource.degress|degress} handler methods.
* See `{@link SCIMMY.Types.Resource~gressHandler}` for more information on implementing handler methods.
*
* > **Note:**
* > For more information on implementing handler methods, see the `{@link SCIMMY.Types.Resource~IngressHandler|IngressHandler}/{@link SCIMMY.Types.Resource~EgressHandler|EgressHandler}/{@link SCIMMY.Types.Resource~DegressHandler|DegressHandler}` type definitions of the `SCIMMY.Types.Resource` class.
*
* ```js
* // Import the necessary methods from the other implementation, and for accessing your data source
* import {parse, filter} from "scim2-parse-filter";
Expand Down
109 changes: 95 additions & 14 deletions src/lib/types/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,56 +59,132 @@ export class Resource {
}

/**
* Handler for ingress/egress/degress of a resource
* @callback SCIMMY.Types.Resource~gressHandler
* @param {SCIMMY.Types.Resource} resource - the resource performing the ingress/egress/degress
* @param {SCIMMY.Types.Schema} [instance] - an instance of the resource type that conforms to the resource's schema
* Handler for ingress of a resource
* @callback SCIMMY.Types.Resource~IngressHandler
* @param {SCIMMY.Types.Resource} resource - the resource performing the ingress
* @param {SCIMMY.Types.Schema} instance - an instance of the resource type that conforms to the resource's schema
* @param {*} [ctx] - external context in which the handler has been called
* @returns {Object} an object to be used to create a new schema instance, whose properties conform to the resource type's schema
* @example
* // Handle a request to create a new resource, or update an existing resource
* async function ingress(resource, instance, ctx) {
* try {
* // Call some external controller to update the resource in your database...
* if (resource.id) return await ResourceController.update(resource.id, instance, ctx);
* // ...or if a resource ID wasn't specified, to create the resource in your database
* else return await ResourceController.create(instance, ctx);
* } catch (ex) {
* switch (ex.message) {
* // Be sure to throw a SCIM 404 error if the specific resource wasn't found...
* case "Not Found":
* throw new SCIMMY.Types.Error(404, null, `Resource ${resource.id} not found`);
* // ...or a SCIM 409 error if a database unique constraint wasn't met...
* case "Not Unique":
* throw new SCIMMY.Types.Error(409, "uniqueness", "Primary email address is not unique");
* // ...and also rethrow any other exceptions as SCIM 500 errors
* default:
* throw new SCIMMY.Types.Error(500, null, ex.message);
* }
* }
* }
*/

/**
* Ingress handler method storage property
* @type {SCIMMY.Types.Resource~gressHandler}
* @type {SCIMMY.Types.Resource~IngressHandler}
* @private
* @abstract
*/
static #ingress;
/**
* Sets the method to be called to consume a resource on create
* @param {SCIMMY.Types.Resource~gressHandler} handler - function to invoke to consume a resource on create
* @param {SCIMMY.Types.Resource~IngressHandler} handler - function to invoke to consume a resource on create
* @returns {SCIMMY.Types.Resource} this resource type class for chaining
* @abstract
*/
static ingress(handler) {
throw new TypeError(`Method 'ingress' not implemented by resource '${this.name}'`);
}

/**
* Handler for egress of a resource
* @callback SCIMMY.Types.Resource~EgressHandler
* @param {SCIMMY.Types.Resource} resource - the resource performing the egress
* @param {*} [ctx] - external context in which the handler has been called
* @returns {Object} an object, to be used to create a new schema instance, whose properties conform to the resource type's schema
* @returns {Object[]} an array of objects, to be used to create new schema instances, whose properties conform to the resource type's schema
* @example
* // Handle a request to retrieve a specific resource, or a list of resources
* async function egress(resource, ctx) {
* try {
* // Call some external controller to retrieve the specified resource from your database...
* if (resource.id) return await ResourceController.findOne(resource.id, ctx);
* // ...or if a resource ID wasn't specified, to retrieve a list of matching resources from your database
* else return await ResourceController.findMany(resource.filter, resource.constraints, ctx);
* } catch (ex) {
* switch (ex.message) {
* // Be sure to throw a SCIM 404 error if the specific resource wasn't found...
* case "Not Found":
* throw new SCIMMY.Types.Error(404, null, `Resource ${resource.id} not found`);
* // ...and also rethrow any other exceptions as SCIM 500 errors
* default:
* throw new SCIMMY.Types.Error(500, null, ex.message);
* }
* }
* }
*/

/**
* Egress handler method storage property
* @type {SCIMMY.Types.Resource~gressHandler}
* @type {SCIMMY.Types.Resource~EgressHandler}
* @private
* @abstract
*/
static #egress;
/**
* Sets the method to be called to retrieve a resource on read
* @param {SCIMMY.Types.Resource~gressHandler} handler - function to invoke to retrieve a resource on read
* @param {SCIMMY.Types.Resource~EgressHandler} handler - function to invoke to retrieve a resource on read
* @returns {SCIMMY.Types.Resource} this resource type class for chaining
* @abstract
*/
static egress(handler) {
throw new TypeError(`Method 'egress' not implemented by resource '${this.name}'`);
}

/**
* Handler for degress of a resource
* @callback SCIMMY.Types.Resource~DegressHandler
* @param {SCIMMY.Types.Resource} resource - the resource performing the degress
* @param {*} [ctx] - external context in which the handler has been called
* @example
* // Handle a request to delete a specific resource
* async function degress(resource, ctx) {
* try {
* // Call some external controller to delete the resource from your database
* await ResourceController.delete(resource.id, ctx);
* } catch (ex) {
* switch (ex.message) {
* // Be sure to throw a SCIM 404 error if the specific resource wasn't found...
* case "Not Found":
* throw new SCIMMY.Types.Error(404, null, `Resource ${resource.id} not found`);
* // ...and also rethrow any other exceptions as SCIM 500 errors
* default:
* throw new SCIMMY.Types.Error(500, null, ex.message);
* }
* }
* }
*/

/**
* Degress handler method storage property
* @type {SCIMMY.Types.Resource~gressHandler}
* @type {SCIMMY.Types.Resource~DegressHandler}
* @private
* @abstract
*/
static #degress;
/**
* Sets the method to be called to dispose of a resource on delete
* @param {SCIMMY.Types.Resource~gressHandler} handler - function to invoke to dispose of a resource on delete
* @param {SCIMMY.Types.Resource~DegressHandler} handler - function to invoke to dispose of a resource on delete
* @returns {SCIMMY.Types.Resource} this resource type class for chaining
* @abstract
*/
Expand All @@ -130,6 +206,7 @@ export class Resource {

/**
* @typedef {Object} SCIMMY.Types.Resource~ResourceType
* @description An object describing a resource type's implementation
* @property {String} id - URN namespace of the resource's SCIM schema definition
* @property {String} name - friendly name of the resource's SCIM schema definition
* @property {String} endpoint - resource type's endpoint, relative to a service provider's base URL
Expand Down Expand Up @@ -227,41 +304,45 @@ export class Resource {
/**
* Calls resource's egress method for data retrieval.
* Wraps the results in valid SCIM list response or single resource syntax.
* @param {*} [ctx] - any additional context information to pass to the egress handler
* @returns {SCIMMY.Messages.ListResponse|SCIMMY.Types.Schema}
* * A collection of resources matching instance's configured filter, if no ID was supplied to resource constructor.
* * The specifically requested resource instance, if an ID was supplied to resource constructor.
* @abstract
*/
read() {
read(ctx) {
throw new TypeError(`Method 'read' not implemented by resource '${this.constructor.name}'`);
}

/**
* Calls resource's ingress method for consumption after unwrapping the SCIM resource
* @param {Object} instance - the raw resource type instance for consumption by ingress method
* @param {*} [ctx] - any additional context information to pass to the ingress handler
* @returns {SCIMMY.Types.Schema} the consumed resource type instance
* @abstract
*/
write(instance) {
write(instance, ctx) {
throw new TypeError(`Method 'write' not implemented by resource '${this.constructor.name}'`);
}

/**
* Retrieves resources via egress method, and applies specified patch operations.
* Emits patched resources for consumption with resource's ingress method.
* @param {Object} message - the PatchOp message to apply to the received resource
* @param {*} [ctx] - any additional context information to pass to the ingress/egress handlers
* @returns {SCIMMY.Types.Schema} the resource type instance after patching and consumption by ingress method
* @abstract
*/
patch(message) {
patch(message, ctx) {
throw new TypeError(`Method 'patch' not implemented by resource '${this.constructor.name}'`);
}

/**
* Calls resource's degress method for disposal of the SCIM resource
* @param {*} [ctx] - any additional context information to pass to the degress handler
* @abstract
*/
dispose() {
dispose(ctx) {
throw new TypeError(`Method 'dispose' not implemented by resource '${this.constructor.name}'`);
}
}
2 changes: 1 addition & 1 deletion test/hooks/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export default {
const target = (!!id ? egress.find(f => f.id === id) : egress);

if (!target) throw new Error("Not found");
else return (Array.isArray(target) ? target : [target]);
else return target;
}
};

Expand Down

0 comments on commit c384c22

Please sign in to comment.