scimmy
Version:
SCIMMY - SCIM m(ade eas)y
757 lines (681 loc) • 34.8 kB
JavaScript
import Types from './types.js';
import Schemas from './schemas.js';
import Messages from './messages.js';
import Config from './config.js';
/**
* SCIM User Resource
* @alias SCIMMY.Resources.User
* @extends {SCIMMY.Types.Resource<SCIMMY.Schemas.User>}
* @summary
* * Handles read/write/patch/dispose operations for SCIM User resources with specified ingress/egress/degress methods.
* * Formats SCIM User resources for transmission/consumption using the `{@link SCIMMY.Schemas.User}` schema class.
*/
class User extends Types.Resource {
/** @implements {SCIMMY.Types.Resource.endpoint} */
static get endpoint() {
return "/Users";
}
/** @private */
static #basepath;
/** @implements {SCIMMY.Types.Resource.basepath<typeof SCIMMY.Resources.User>} */
static basepath(path) {
if (path === undefined) return User.#basepath;
else User.#basepath = (path.endsWith(User.endpoint) ? path : `${path}${User.endpoint}`);
return User;
}
/**
* @implements {SCIMMY.Types.Resource.schema}
* @type {typeof SCIMMY.Schemas.User}
*/
static get schema() {
return Schemas.User;
}
/** @implements {SCIMMY.Types.Resource.extend<typeof SCIMMY.Resources.User>} */
static extend(...args) {
return super.extend(...args);
}
/** @private */
static #ingress = () => {
throw new Types.Error(501, null, "Method 'ingress' not implemented by resource 'User'");
};
/** @implements {SCIMMY.Types.Resource.ingress<typeof SCIMMY.Resources.User, SCIMMY.Schemas.User>} */
static ingress(handler) {
User.#ingress = handler;
return User;
}
/** @private */
static #egress = () => {
throw new Types.Error(501, null, "Method 'egress' not implemented by resource 'User'");
};
/** @implements {SCIMMY.Types.Resource.egress<typeof SCIMMY.Resources.User, SCIMMY.Schemas.User>} */
static egress(handler) {
User.#egress = handler;
return User;
}
/** @private */
static #degress = () => {
throw new Types.Error(501, null, "Method 'degress' not implemented by resource 'User'");
};
/** @implements {SCIMMY.Types.Resource.degress<typeof SCIMMY.Resources.User>} */
static degress(handler) {
User.#degress = handler;
return User;
}
/**
* Instantiate a new SCIM User resource and parse any supplied parameters
* @internal
*/
constructor(...params) {
super(...params);
}
/**
* @implements {SCIMMY.Types.Resource#read}
* @example
* // Retrieve user with ID "1234"
* await new SCIMMY.Resources.User("1234").read();
* @example
* // Retrieve users with an email ending in "@example.com"
* await new SCIMMY.Resources.User({filter: 'email.value ew "@example.com"'}).read();
*/
async read(ctx) {
try {
const source = await User.#egress(this, ctx);
const target = (this.id ? [source].flat().shift() : source);
// If not looking for a specific resource, make sure egress returned an array
if (!this.id && Array.isArray(target)) return new Messages.ListResponse(target
.map(u => new Schemas.User(u, "out", User.basepath(), this.attributes)), this.constraints);
// For specific resources, make sure egress returned an object
else if (target instanceof Object) return new Schemas.User(target, "out", User.basepath(), this.attributes);
// Otherwise, egress has not been implemented correctly
else throw new Types.Error(500, null, `Unexpected ${target === undefined ? "empty" : "invalid"} value returned by egress handler`);
} catch (ex) {
if (ex instanceof Types.Error) throw ex;
else if (ex instanceof TypeError) throw new Types.Error(400, "invalidValue", ex.message);
else throw new Types.Error(404, null, `Resource ${this.id} not found`);
}
}
/**
* @implements {SCIMMY.Types.Resource#write}
* @example
* // Create a new user with userName "someGuy"
* await new SCIMMY.Resources.User().write({userName: "someGuy"});
* @example
* // Set userName attribute to "someGuy" for user with ID "1234"
* await new SCIMMY.Resources.User("1234").write({userName: "someGuy"});
*/
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 {
const target = await User.#ingress(this, new Schemas.User(instance, "in"), ctx);
// Make sure ingress returned an object
if (target instanceof Object) return new Schemas.User(target, "out", User.basepath(), this.attributes);
// Otherwise, ingress has not been implemented correctly
else throw new Types.Error(500, null, `Unexpected ${target === undefined ? "empty" : "invalid"} value returned by ingress handler`);
} catch (ex) {
if (ex instanceof Types.Error) throw ex;
else if (ex instanceof TypeError) throw new Types.Error(400, "invalidValue", ex.message);
else throw new Types.Error(404, null, `Resource ${this.id} not found`);
}
}
/**
* @implements {SCIMMY.Types.Resource#patch}
* @see SCIMMY.Messages.PatchOp
* @example
* // 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, ctx) {
if (!this.id)
throw new Types.Error(404, null, "PATCH operation must target a specific resource");
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");
return await new Messages.PatchOp(message)
.apply(await this.read(ctx), async (instance) => await this.write(instance, ctx))
.then(instance => !instance ? undefined : new Schemas.User(instance, "out", User.basepath(), this.attributes));
}
/**
* @implements {SCIMMY.Types.Resource#dispose}
* @example
* // Delete user with ID "1234"
* await new SCIMMY.Resources.User("1234").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, ctx);
} catch (ex) {
if (ex instanceof Types.Error) throw ex;
else if (ex instanceof TypeError) throw new Types.Error(500, null, ex.message);
else throw new Types.Error(404, null, `Resource ${this.id} not found`);
}
}
}
/**
* SCIM Group Resource
* @alias SCIMMY.Resources.Group
* @extends {SCIMMY.Types.Resource<SCIMMY.Schemas.Group>}
* @summary
* * Handles read/write/patch/dispose operations for SCIM Group resources with specified ingress/egress/degress methods.
* * Formats SCIM Group resources for transmission/consumption using the `{@link SCIMMY.Schemas.Group}` schema class.
*/
class Group extends Types.Resource {
/** @implements {SCIMMY.Types.Resource.endpoint} */
static get endpoint() {
return "/Groups";
}
/** @private */
static #basepath;
/** @implements {SCIMMY.Types.Resource.basepath<typeof SCIMMY.Resources.Group>} */
static basepath(path) {
if (path === undefined) return Group.#basepath;
else Group.#basepath = (path.endsWith(Group.endpoint) ? path : `${path}${Group.endpoint}`);
return Group;
}
/**
* @implements {SCIMMY.Types.Resource.schema}
* @type {typeof SCIMMY.Schemas.Group}
*/
static get schema() {
return Schemas.Group;
}
/** @implements {SCIMMY.Types.Resource.extend<typeof SCIMMY.Resources.Group>} */
static extend(...args) {
return super.extend(...args);
}
/** @private */
static #ingress = () => {
throw new Types.Error(501, null, "Method 'ingress' not implemented by resource 'Group'");
};
/** @implements {SCIMMY.Types.Resource.ingress<typeof SCIMMY.Resources.Group, SCIMMY.Schemas.Group>} */
static ingress(handler) {
Group.#ingress = handler;
return Group;
}
/** @private */
static #egress = () => {
throw new Types.Error(501, null, "Method 'egress' not implemented by resource 'Group'");
};
/** @implements {SCIMMY.Types.Resource.egress<typeof SCIMMY.Resources.Group, SCIMMY.Schemas.Group>} */
static egress(handler) {
Group.#egress = handler;
return Group;
}
/** @private */
static #degress = () => {
throw new Types.Error(501, null, "Method 'degress' not implemented by resource 'Group'");
};
/** @implements {SCIMMY.Types.Resource.degress<typeof SCIMMY.Resources.Group>} */
static degress(handler) {
Group.#degress = handler;
return Group;
}
/**
* Instantiate a new SCIM Group resource and parse any supplied parameters
* @internal
*/
constructor(...params) {
super(...params);
}
/**
* @implements {SCIMMY.Types.Resource#read}
* @example
* // Retrieve group with ID "1234"
* await new SCIMMY.Resources.Group("1234").read();
* @example
* // Retrieve groups with a group name starting with "A"
* await new SCIMMY.Resources.Group({filter: 'displayName sw "A"'}).read();
*/
async read(ctx) {
try {
const source = await Group.#egress(this, ctx);
const target = (this.id ? [source].flat().shift() : source);
// If not looking for a specific resource, make sure egress returned an array
if (!this.id && Array.isArray(target)) return new Messages.ListResponse(target
.map(u => new Schemas.Group(u, "out", Group.basepath(), this.attributes)), this.constraints);
// For specific resources, make sure egress returned an object
else if (target instanceof Object) return new Schemas.Group(target, "out", Group.basepath(), this.attributes);
// Otherwise, egress has not been implemented correctly
else throw new Types.Error(500, null, `Unexpected ${target === undefined ? "empty" : "invalid"} value returned by egress handler`);
} catch (ex) {
if (ex instanceof Types.Error) throw ex;
else if (ex instanceof TypeError) throw new Types.Error(400, "invalidValue", ex.message);
else throw new Types.Error(404, null, `Resource ${this.id} not found`);
}
}
/**
* @implements {SCIMMY.Types.Resource#write}
* @example
* // Create a new group with displayName "A Group"
* await new SCIMMY.Resources.Group().write({displayName: "A Group"});
* @example
* // Set members attribute for group with ID "1234"
* await new SCIMMY.Resources.Group("1234").write({members: [{value: "5678"}]});
*/
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 {
const target = await Group.#ingress(this, new Schemas.Group(instance, "in"), ctx);
// Make sure ingress returned an object
if (target instanceof Object) return new Schemas.Group(target, "out", Group.basepath(), this.attributes);
// Otherwise, ingress has not been implemented correctly
else throw new Types.Error(500, null, `Unexpected ${target === undefined ? "empty" : "invalid"} value returned by ingress handler`);
} catch (ex) {
if (ex instanceof Types.Error) throw ex;
else if (ex instanceof TypeError) throw new Types.Error(400, "invalidValue", ex.message);
else throw new Types.Error(404, null, `Resource ${this.id} not found`);
}
}
/**
* @implements {SCIMMY.Types.Resource#patch}
* @see SCIMMY.Messages.PatchOp
* @example
* // 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, ctx) {
if (!this.id)
throw new Types.Error(404, null, "PATCH operation must target a specific resource");
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");
return await new Messages.PatchOp(message)
.apply(await this.read(ctx), async (instance) => await this.write(instance, ctx))
.then(instance => !instance ? undefined : new Schemas.Group(instance, "out", Group.basepath(), this.attributes));
}
/**
* @implements {SCIMMY.Types.Resource#dispose}
* @example
* // Delete group with ID "1234"
* await new SCIMMY.Resources.Group("1234").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, ctx);
} catch (ex) {
if (ex instanceof Types.Error) throw ex;
else if (ex instanceof TypeError) throw new Types.Error(500, null, ex.message);
else throw new Types.Error(404, null, `Resource ${this.id} not found`);
}
}
}
/**
* SCIM Schema Resource
* @alias SCIMMY.Resources.Schema
* @extends {SCIMMY.Types.Resource<SCIMMY.Types.SchemaDefinition~SchemaDescription>}
* @summary
* * Formats SCIM schema definition implementations declared in `{@link SCIMMY.Schemas}` for transmission/consumption according to the Schema Definition schema set out in [RFC7643§7](https://datatracker.ietf.org/doc/html/rfc7643#section-7).
*/
class Schema extends Types.Resource {
/** @implements {SCIMMY.Types.Resource.endpoint} */
static get endpoint() {
return "/Schemas";
}
/** @private */
static #basepath;
/** @implements {SCIMMY.Types.Resource.basepath<typeof SCIMMY.Resources.Schema>} */
static basepath(path) {
if (path === undefined) return Schema.#basepath;
else Schema.#basepath = (path.endsWith(Schema.endpoint) ? path : `${path}${Schema.endpoint}`);
return Schema;
}
/**
* @overrides {SCIMMY.Types.Resource.extend}
* @throws {TypeError} SCIM 'Schema' resource does not support extension
*/
static extend() {
throw new TypeError("SCIM 'Schema' resource does not support extension");
}
/**
* Instantiate a new SCIM Schema resource and parse any supplied parameters
* @internal
*/
constructor(id, config) {
// Bail out if a resource is requested by filter
if (!!((typeof id === "string" ? config : id) ?? {})?.filter)
throw new Types.Error(403, null, "Schema does not support retrieval by filter");
super(id, config);
}
/** @implements {SCIMMY.Types.Resource#read} */
async read() {
if (!this.id) {
return new Messages.ListResponse(Schemas.declared().map((S) => S.describe(Schema.basepath())));
} else {
try {
return Schemas.declared(this.id).describe(Schema.basepath());
} catch (ex) {
throw new Types.Error(404, null, `Schema ${this.id} not found`);
}
}
}
}
/**
* SCIM ResourceType Resource
* @alias SCIMMY.Resources.ResourceType
* @extends {SCIMMY.Types.Resource<SCIMMY.Schemas.ResourceType>}
* @summary
* * Formats SCIM Resource Type implementations declared in `{@link SCIMMY.Resources}` for transmission/consumption according to the ResourceType schema set out in [RFC7643§6](https://datatracker.ietf.org/doc/html/rfc7643#section-6).
*/
class ResourceType extends Types.Resource {
/** @implements {SCIMMY.Types.Resource.endpoint} */
static get endpoint() {
return "/ResourceTypes";
}
/** @private */
static #basepath;
/** @implements {SCIMMY.Types.Resource.basepath<typeof SCIMMY.Resources.ResourceType>} */
static basepath(path) {
if (path === undefined) return ResourceType.#basepath;
else ResourceType.#basepath = (path.endsWith(ResourceType.endpoint) ? path : `${path}${ResourceType.endpoint}`);
return ResourceType;
}
/**
* @overrides {SCIMMY.Types.Resource.extend}
* @throws {TypeError} SCIM 'ResourceType' resource does not support extension
*/
static extend() {
throw new TypeError("SCIM 'ResourceType' resource does not support extension");
}
/**
* Instantiate a new SCIM ResourceType resource and parse any supplied parameters
* @internal
*/
constructor(id, config) {
// Bail out if a resource is requested by filter
if (!!((typeof id === "string" ? config : id) ?? {})?.filter)
throw new Types.Error(403, null, "ResourceType does not support retrieval by filter");
super(id, config);
}
/** @implements {SCIMMY.Types.Resource#read} */
async read() {
if (!this.id) {
return new Messages.ListResponse(Object.entries(Resources.declared())
.map(([,R]) => new Schemas.ResourceType(R.describe(), ResourceType.basepath())));
} else {
try {
return new Schemas.ResourceType(Resources.declared(this.id).describe(), ResourceType.basepath());
} catch {
throw new Types.Error(404, null, `ResourceType ${this.id} not found`);
}
}
}
}
/**
* SCIM ServiceProviderConfig Resource
* @alias SCIMMY.Resources.ServiceProviderConfig
* @extends {SCIMMY.Types.Resource<SCIMMY.Schemas.ServiceProviderConfig>}
* @summary
* * Formats SCIM Service Provider Configuration set in `{@link SCIMMY.Config}` for transmission/consumption according to the Service Provider Configuration schema set out in [RFC7643§5](https://datatracker.ietf.org/doc/html/rfc7643#section-5).
*/
class ServiceProviderConfig extends Types.Resource {
/** @implements {SCIMMY.Types.Resource.endpoint} */
static get endpoint() {
return "/ServiceProviderConfig";
}
/** @private */
static #basepath;
/** @implements {SCIMMY.Types.Resource.basepath<typeof SCIMMY.Resources.ServiceProviderConfig>} */
static basepath(path) {
if (path === undefined) return ServiceProviderConfig.#basepath;
else ServiceProviderConfig.#basepath = (path.endsWith(ServiceProviderConfig.endpoint) ? path : `${path}${ServiceProviderConfig.endpoint}`);
return ServiceProviderConfig;
}
/**
* @overrides {SCIMMY.Types.Resource.extend}
* @throws {TypeError} SCIM 'ServiceProviderConfig' resource does not support extension
*/
static extend() {
throw new TypeError("SCIM 'ServiceProviderConfig' resource does not support extension");
}
/**
* Instantiate a new SCIM ServiceProviderConfig resource and parse any supplied parameters
* @internal
*/
constructor(...params) {
super(...params);
// Bail out if a resource is requested with filter or attribute properties
if (!!Object.keys(this).length)
throw new Types.Error(403, null, "ServiceProviderConfig does not support retrieval by filter");
}
/** @implements {SCIMMY.Types.Resource#read} */
async read() {
return new Schemas.ServiceProviderConfig(Config.get(), ServiceProviderConfig.basepath());
}
}
/**
* SCIMMY Resources Container Class
* @module scimmy/resources
* @namespace SCIMMY.Resources
* @description
* SCIMMY provides a singleton class, `SCIMMY.Resources`, that is used to declare resource types implemented by a SCIM Service Provider.
* It also provides access to supplied implementations of core resource types that can be used to easily support well-known resource types.
* It is also used to retrieve a service provider's declared resource types to be sent via the ResourceTypes HTTP endpoint.
*
* > **Note:**
* > The `SCIMMY.Resources` class is a singleton, which means that declared resource types
* > will remain the same, regardless of where the class is accessed from within your code.
*
* ## Declaring Resource Types
* Resource type implementations can be declared by calling `{@link SCIMMY.Resources.declare}`.
* This method will add the given resource type implementation to the list of declared resource types, and automatically
* declare the resource type's schema, and any schema extensions it may have, to the `{@link SCIMMY.Schemas}` class.
* ```
* // Declare several resource types at once
* SCIMMY.Resources.declare(SCIMMY.Resources.User, {}).declare(SCIMMY.Resources.Group, {});
* ```
*
* Once declared, resource type implementations are made available to the `{@link SCIMMY.Resources.ResourceType}`
* resource type, which handles formatting them for transmission/consumption according to the ResourceType schema
* set out in [RFC7643§6](https://datatracker.ietf.org/doc/html/rfc7643#section-6).
*
* Each resource type implementation must be declared with a unique name, and each name can only be declared once.
* Attempting to declare a resource type with a name that has already been declared will throw a TypeError with the
* message `"Resource '<name>' already declared"`, where `<name>` is the name of the resource type.
*
* Similarly, each resource type implementation can only be declared under one name.
* Attempting to declare an existing resource type under a new name will throw a TypeError with the message
* `"Resource '<name>' already declared with name '<existing>'"`, where `<name>` and `<existing>` are the targeted name
* and existing name, respectively, of the resource type.
*
* ```
* // Declaring a resource type under a different name
* class User extends SCIMMY.Types.Resource {/ Your resource type implementation /}
* SCIMMY.Resources.declare(User, "CustomUser");
* ```
*
* ### Extending Resource Types
* With the exception of the `ResourceType`, `Schema`, and `ServiceProviderConfig` resources, resource type implementations
* can have schema extensions attached to them via the `{@link SCIMMY.Types.Resource.extend extend}` method inherited from
* the `{@link SCIMMY.Types.Resource}` class. Schema extensions added to resource type implementations will automatically
* be included in the `schemaExtensions` attribute when formatted by the `ResourceType` resource, and the extension's
* schema definition declared to the `{@link SCIMMY.Schemas}` class.
*
* Resource type implementations can be extended:
* * At the time of declaration via the declaration config object:
* ```
* // Add the EnterpriseUser schema as a required extension at declaration
* SCIMMY.Resources.declare(SCIMMY.Resources.User, {
* extensions: [{schema: SCIMMY.Schemas.EnterpriseUser, required: true}]
* });
* ```
* * Immediately after declaration via the resource's `{@link SCIMMY.Types.Resource.extend extend}` method:
* ```
* // Add the EnterpriseUser schema as a required extension after declaration
* SCIMMY.Resources.declare(SCIMMY.Resources.User).extend(SCIMMY.Schemas.EnterpriseUser, true);
* ```
* * Before or during declaration, directly on the resource, via the resource's `{@link SCIMMY.Types.Resource.extend extend}` method:
* ```
* // Add the EnterpriseUser schema as an optional extension before declaration
* SCIMMY.Resources.User.extend(SCIMMY.Schemas.EnterpriseUser, false);
* SCIMMY.Resources.declare(SCIMMY.Resources.User);
*
* // Add the EnterpriseUser schema as a required extension during declaration
* SCIMMY.Resources.declare(SCIMMY.Resources.User.extend(SCIMMY.Schemas.EnterpriseUser, true));
* ```
* * Any time after declaration, directly on the retrieved resource, via the resource's `{@link SCIMMY.Types.Resource.extend extend}` method:
* ```
* // Add the EnterpriseUser schema as a required extension after declaration
* SCIMMY.Resources.declared("User").extend(SCIMMY.Schemas.EnterpriseUser, true);
* ```
*
* ## Retrieving Declared Types
* Declared resource type implementations can be retrieved via the `{@link SCIMMY.Resources.declared}` method.
* * All currently declared resource types can be retrieved by calling the method with no arguments.
* ```
* // Returns a cloned object with resource type names as keys, and resource type implementation classes as values
* SCIMMY.Resources.declared();
* ```
* * Specific declared implementations can be retrieved by calling the method with the resource type name string.
* This will return the same resource type implementation class that was previously declared.
* ```
* // Returns the declared resource matching the specified name, or undefined if no resource matched the name
* SCIMMY.Resources.declared("MyResourceType");
* ```
*
* @example <caption>Basic usage with provided resource type implementations</caption>
* SCIMMY.Resources.declare(SCIMMY.Resources.User)
* .ingress((resource, data) => {/ Your handler for creating or modifying user resources /})
* .egress((resource) => {/ Your handler for retrieving user resources /})
* .degress((resource) => {/ Your handler for deleting user resources /});
* @example <caption>Advanced usage with custom resource type implementations</caption>
* SCIMMY.Resources.declare(class MyResourceType extends SCIMMY.Types.Resource {
* read() {/ Your handler for retrieving resources /})
* write(data) {/ Your handler for creating or modifying resources /}
* dispose() {/ Your handler for deleting resources /})
* // ...the rest of your resource type implementation //
* });
*/
class Resources {
/**
* Store internal resources to prevent declaration
* @private
*/
static #internals = [Schema, ResourceType, ServiceProviderConfig];
/**
* Store declared resources for later retrieval
* @private
*/
static #declared = {};
// Expose built-in resources without "declaring" them
static Schema = Schema;
static ResourceType = ResourceType;
static ServiceProviderConfig = ServiceProviderConfig;
static User = User;
static Group = Group;
/**
* Register a resource implementation and return it for chained configuration
* @template {typeof SCIMMY.Types.Resource<any>} R
* @overload
* @param {R} resource - the resource type implementation to register
* @param {String} [config] - the explicit name to register the resource implementation with
* @returns {R} the registered resource type class for chaining
*/
/**
* Register a resource implementation with specific config, returning Resources class for chained registrations
* @template {typeof SCIMMY.Types.Resource<any>} R
* @overload
* @param {R} resource - the resource type implementation to register
* @param {Object} config - the configuration to feed to the resource being registered
* @returns {typeof SCIMMY.Resources} the Resources class for chaining
*/
/**
* Register a resource implementation for exposure as a ResourceType
* @param {typeof SCIMMY.Types.Resource} resource - the resource type implementation to register
* @param {Object|String} [config] - the configuration to feed to the resource being registered, or the name of the resource type implementation if different to the class name
* @returns {typeof SCIMMY.Resources|typeof SCIMMY.Types.Resource} the Resources class or registered resource type class for chaining
*/
static declare(resource, config) {
// Make sure the registering resource is valid
if (!resource || !(resource.prototype instanceof Types.Resource))
throw new TypeError("Registering resource must be of type 'Resource'");
// Make sure config is valid, if supplied
if (config !== undefined && (typeof config !== "string" && (typeof config !== "object" || Array.isArray(config))))
throw new TypeError("Resource declaration expected 'config' parameter to be either a name string or configuration object");
// Refuse to declare internal resources
if (Resources.#internals.includes(resource))
throw new TypeError(`Refusing to declare internal resource implementation '${resource.name}'`);
// Source name from resource if config is an object
let name = (typeof config === "string" ? config : resource?.name);
if (typeof config === "object") name = config.name ?? name;
// Prevent registering a resource implementation under a name that already exists
if (!!Resources.#declared[name] && Resources.#declared[name] !== resource)
throw new TypeError(`Resource '${name}' already declared`);
// Prevent registering an existing resource implementation under a different name
else if (Object.values(Resources.#declared).some(r => r === resource) && Resources.#declared[name] !== resource)
throw new TypeError(`Resource '${name}' already declared with name '${Object.entries(Resources.#declared).find(([n, r]) => r === resource).shift()}'`);
// All good, register the resource implementation
else if (!Resources.#declared[name])
Resources.#declared[name] = resource;
// Set up the resource if a config object was supplied
if (typeof config === "object") {
// Register supplied basepath
if (typeof config.basepath === "string")
Resources.#declared[name].basepath(config.basepath);
// Register supplied ingress, egress, and degress methods
if (typeof config.ingress === "function")
Resources.#declared[name].ingress(async (...r) => await config.ingress(...r));
if (typeof config.egress === "function")
Resources.#declared[name].egress(async (...r) => await config.egress(...r));
if (typeof config.degress === "function")
Resources.#declared[name].degress(async (...r) => await config.degress(...r));
// Register any supplied schema extensions
if (Array.isArray(config.extensions)) {
for (let {schema, attributes, required} of config.extensions) {
Resources.#declared[name].extend(schema ?? attributes, required);
}
}
}
// Declare the resource type implementation's schema!
Schemas.declare(resource.schema.definition);
// If config was supplied, return Resources, otherwise return the registered resource
return (typeof config === "object" ? Resources : resource);
}
/**
* Get all registered resource implementations
* @overload
* @returns {Record<String, typeof SCIMMY.Types.Resource>} a containing object with all registered resource implementations
*/
/**
* Get the registered resource type implementation for the given name
* @overload
* @param {String} resource - registered name of resource to retrieve
* @returns {typeof SCIMMY.Types.Resource} the registered resource type implementation with matching name
*/
/**
* Query the registration status of a given resource type implementation
* @template {typeof SCIMMY.Types.Resource<any>} R
* @overload
* @param {R} resource - the resource type implementation to query registration status for
* @returns {Boolean} the registration status of the specified resource type implementation class
*/
/**
* Get registration status of specific resource implementation, or get all registered resource implementations
* @param {typeof SCIMMY.Types.Resource|String} [resource] - the resource implementation or name to query registration status for
* @returns {Record<String, typeof SCIMMY.Types.Resource>|typeof SCIMMY.Types.Resource|Boolean}
* * A containing object with registered resource implementations for exposure as ResourceTypes, if no arguments are supplied.
* * The registered resource type implementation with matching name, or undefined, if a string argument is supplied.
* * The registration status of the specified resource implementation, if a class extending `SCIMMY.Types.Resource` is supplied.
*/
static declared(resource) {
// If no resource specified, return declared resources
if (!resource) return {...Resources.#declared};
// If resource is a string, find and return the matching resource type
else if (typeof resource === "string") return Resources.#declared[resource];
// If the resource is an instance of Resource, see if it is already registered
else if (resource.prototype instanceof Types.Resource) return Resources.#declared[resource.name] === resource;
// Otherwise, the resource isn't registered...
else return false;
}
}
export { Group, ResourceType, Schema, ServiceProviderConfig, User, Resources as default };