diff --git a/src/main.ts b/src/main.ts index a557949..0e4a848 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,10 @@ /** - * People Manager Module Boot Script - * - * This script is executed when the people_manager module is loaded. - * It initializes the peopleStore which manages contacts and address books state. + * People Manager Module Boot */ -console.log('[PeopleManager] Booting People Manager module...') +console.log('[People Manager] Booting module...') -console.log('[PeopleManager] People Manager module booted successfully') +console.log('[People Manager] Module booted successfully...') // CSS will be injected by build process //export const css = ['__CSS_FILENAME_PLACEHOLDER__'] diff --git a/src/models/collection.ts b/src/models/collection.ts index 0abcce5..3c92203 100644 --- a/src/models/collection.ts +++ b/src/models/collection.ts @@ -2,50 +2,131 @@ * Class model for Collection Interface */ -import type { - CollectionInterface, - CollectionContentsInterface, - CollectionPermissionsInterface, - CollectionRolesInterface -} from "@/types/collection"; +import type { CollectionContentTypes, CollectionInterface, CollectionPropertiesInterface } from "@/types/collection"; export class CollectionObject implements CollectionInterface { _data!: CollectionInterface; - + constructor() { this._data = { - '@type': 'people:collection', - provider: null, - service: null, - in: null, - id: null, - label: null, - description: null, - priority: null, - visibility: null, - color: null, - enabled: false, + provider: '', + service: '', + collection: null, + identifier: '', signature: null, - permissions: {}, - roles: {}, - contents: {}, + created: null, + modified: null, + properties: new CollectionPropertiesObject(), }; } - fromJson(data: CollectionInterface) : CollectionObject { + fromJson(data: CollectionInterface): CollectionObject { this._data = data; + if (data.properties) { + this._data.properties = new CollectionPropertiesObject().fromJson(data.properties as CollectionPropertiesInterface); + } return this; } toJson(): CollectionInterface { - return this._data; + const json = { ...this._data }; + if (this._data.properties instanceof CollectionPropertiesObject) { + json.properties = this._data.properties.toJson(); + } + return json; } clone(): CollectionObject { - const cloned = new CollectionObject() - cloned._data = JSON.parse(JSON.stringify(this._data)) - return cloned + const cloned = new CollectionObject(); + cloned._data = { ...this._data }; + cloned._data.properties = this.properties.clone(); + return cloned; + } + + /** Immutable Properties */ + + get provider(): string { + return this._data.provider; + } + + get service(): string | number { + return this._data.service; + } + + get collection(): string | number | null { + return this._data.collection; + } + + get identifier(): string | number { + return this._data.identifier; + } + + get signature(): string | null | undefined { + return this._data.signature; + } + + get created(): string | null | undefined { + return this._data.created; + } + + get modified(): string | null | undefined { + return this._data.modified; + } + + get properties(): CollectionPropertiesObject { + if (this._data.properties instanceof CollectionPropertiesObject) { + return this._data.properties; + } + + if (this._data.properties) { + const hydrated = new CollectionPropertiesObject().fromJson(this._data.properties as CollectionPropertiesInterface); + this._data.properties = hydrated; + return hydrated; + } + + return new CollectionPropertiesObject(); + } + + set properties(value: CollectionPropertiesObject) { + if (value instanceof CollectionPropertiesObject) { + this._data.properties = value as any; + } else { + this._data.properties = value; + } + } +} + +export class CollectionPropertiesObject implements CollectionPropertiesInterface { + + _data!: CollectionPropertiesInterface; + + constructor() { + this._data = { + '@type': 'people:collection', + version: 1, + content: [], + label: '', + description: null, + rank: null, + visibility: null, + color: null, + }; + } + + fromJson(data: CollectionPropertiesInterface): CollectionPropertiesObject { + this._data = data; + return this; + } + + toJson(): CollectionPropertiesInterface { + return this._data; + } + + clone(): CollectionPropertiesObject { + const cloned = new CollectionPropertiesObject(); + cloned._data = { ...this._data }; + return cloned; } /** Immutable Properties */ @@ -54,86 +135,54 @@ export class CollectionObject implements CollectionInterface { return this._data['@type']; } - get provider(): string | null { - return this._data.provider + get version(): number { + return this._data.version; } - get service(): string | null { - return this._data.service - } - - get in(): number | string | null { - return this._data.in - } - - get id(): number | string | null { - return this._data.id - } - - get signature(): string | null { - return this._data.signature - } - - get permissions(): CollectionPermissionsInterface { - return this._data.permissions - } - - get roles(): CollectionRolesInterface { - return this._data.roles - } - - get contents(): CollectionContentsInterface { - return this._data.contents + get content(): CollectionContentTypes[] { + return this._data.content || []; } /** Mutable Properties */ - get label(): string | null { - return this._data.label - } + get label(): string { + return this._data.label || ''; + } - set label(value: string ) { - this._data.label = value - } + set label(value: string) { + this._data.label = value; + } get description(): string | null { - return this._data.description + return this._data.description; } - set description(value: string ) { - this._data.description = value + set description(value: string | null) { + this._data.description = value; } - get priority(): number | null { - return this._data.priority + get rank(): number | null { + return this._data.rank; } - set priority(value: number ) { - this._data.priority = value + set rank(value: number | null) { + this._data.rank = value; } - get visibility(): string | null { - return this._data.visibility + get visibility(): boolean | null { + return this._data.visibility; } - set visibility(value: string ) { - this._data.visibility = value + set visibility(value: boolean | null) { + this._data.visibility = value; } get color(): string | null { - return this._data.color + return this._data.color; } - set color(value: string ) { - this._data.color = value + set color(value: string | null) { + this._data.color = value; } - get enabled(): boolean { - return this._data.enabled - } - - set enabled(value: boolean ) { - this._data.enabled = value - } - } \ No newline at end of file diff --git a/src/models/entity.ts b/src/models/entity.ts index b631ebd..69c1339 100644 --- a/src/models/entity.ts +++ b/src/models/entity.ts @@ -16,138 +16,92 @@ export class EntityObject implements EntityInterface { constructor() { this._data = { - '@type': 'people:entity', - version: 1, - in: null, - id: null, - createdOn: null, - createdBy: null, - modifiedOn: null, - modifiedBy: null, + provider: '', + service: '', + collection: '', + identifier: '', signature: null, - data: null, + created: null, + modified: null, + properties: new IndividualObject(), }; } fromJson(data: EntityInterface) : EntityObject { this._data = data; - if (data.data) { - const type = data.data.type; + if (data.properties) { + const type = data.properties.type; if (type === 'organization') { - this._data.data = new OrganizationObject().fromJson(data.data as OrganizationInterface); + this._data.properties = new OrganizationObject().fromJson(data.properties as OrganizationInterface); } else if (type === 'group') { - this._data.data = new GroupObject().fromJson(data.data as GroupInterface); + this._data.properties = new GroupObject().fromJson(data.properties as GroupInterface); } else { - this._data.data = new IndividualObject().fromJson(data.data as IndividualInterface); + this._data.properties = new IndividualObject().fromJson(data.properties as IndividualInterface); } - } else { - this._data.data = null; } return this; } toJson(): EntityInterface { const json = { ...this._data }; - if (this._data.data instanceof IndividualObject || - this._data.data instanceof OrganizationObject || - this._data.data instanceof GroupObject) { - json.data = this._data.data.toJson(); + if (this._data.properties instanceof IndividualObject || + this._data.properties instanceof OrganizationObject || + this._data.properties instanceof GroupObject) { + json.properties = this._data.properties.toJson(); } return json; } clone(): EntityObject { - const cloned = new EntityObject() - cloned._data = JSON.parse(JSON.stringify(this._data)) - return cloned + const cloned = new EntityObject(); + cloned._data = { ...this._data }; + return cloned; } /** Immutable Properties */ - - get '@type'(): string { - return this._data['@type']; + + get provider(): string { + return this._data.provider; } - get in(): number | string | null { - return this._data.in; + get service(): string { + return this._data.service; } - get id(): number | string | null { - return this._data.id; + get collection(): string | number { + return this._data.collection; } - get version(): number { - return this._data.version; - } - - get createdOn(): Date | null { - return this._data.createdOn; - } - - get createdBy(): string | null { - return this._data.createdBy; - } - - get modifiedOn(): Date | null { - return this._data.modifiedOn; - } - - get modifiedBy(): string | null { - return this._data.modifiedBy; + get identifier(): string | number { + return this._data.identifier; } get signature(): string | null { return this._data.signature; } - /** Mutable Properties */ - - set createdOn(value: Date | null) { - this._data.createdOn = value; + get created(): string | null { + return this._data.created; } - set createdBy(value: string | null) { - this._data.createdBy = value; + get modified(): string | null { + return this._data.modified; } - set modifiedOn(value: Date | null) { - this._data.modifiedOn = value; - } - - set modifiedBy(value: string | null) { - this._data.modifiedBy = value; - } - - set signature(value: string | null) { - this._data.signature = value; - } - - get data(): IndividualObject | OrganizationObject | GroupObject | null { - if (this._data.data instanceof IndividualObject || - this._data.data instanceof OrganizationObject || - this._data.data instanceof GroupObject) { - return this._data.data; + get properties(): IndividualObject | OrganizationObject | GroupObject { + if (this._data.properties instanceof IndividualObject || + this._data.properties instanceof OrganizationObject || + this._data.properties instanceof GroupObject) { + return this._data.properties; } - if (this._data.data) { - const type = this._data.data.type; - let hydrated; - if (type === 'organization') { - hydrated = new OrganizationObject().fromJson(this._data.data as OrganizationInterface); - } else if (type === 'group') { - hydrated = new GroupObject().fromJson(this._data.data as GroupInterface); - } else { - hydrated = new IndividualObject().fromJson(this._data.data as IndividualInterface); - } - this._data.data = hydrated; - return hydrated; - } - - return null; + const defaultProperties = new IndividualObject(); + this._data.properties = defaultProperties; + return defaultProperties; } - set data(value: IndividualObject | OrganizationObject | GroupObject | null) { - this._data.data = value; + set properties(value: IndividualObject | OrganizationObject | GroupObject) { + this._data.properties = value; } } \ No newline at end of file diff --git a/src/models/identity.ts b/src/models/identity.ts new file mode 100644 index 0000000..3291090 --- /dev/null +++ b/src/models/identity.ts @@ -0,0 +1,196 @@ +/** + * Identity implementation classes for Mail Manager services + */ + +import type { + ServiceIdentity, + ServiceIdentityNone, + ServiceIdentityBasic, + ServiceIdentityToken, + ServiceIdentityOAuth, + ServiceIdentityCertificate +} from '@/types/service'; + +/** + * Base Identity class + */ +export abstract class Identity { + abstract toJson(): ServiceIdentity; + + static fromJson(data: ServiceIdentity): Identity { + switch (data.type) { + case 'NA': + return IdentityNone.fromJson(data); + case 'BA': + return IdentityBasic.fromJson(data); + case 'TA': + return IdentityToken.fromJson(data); + case 'OA': + return IdentityOAuth.fromJson(data); + case 'CC': + return IdentityCertificate.fromJson(data); + default: + throw new Error(`Unknown identity type: ${(data as any).type}`); + } + } +} + +/** + * No authentication + */ +export class IdentityNone extends Identity { + readonly type = 'NA' as const; + + static fromJson(_data: ServiceIdentityNone): IdentityNone { + return new IdentityNone(); + } + + toJson(): ServiceIdentityNone { + return { + type: this.type + }; + } +} + +/** + * Basic authentication (username/password) + */ +export class IdentityBasic extends Identity { + readonly type = 'BA' as const; + identity: string; + secret: string; + + constructor(identity: string = '', secret: string = '') { + super(); + this.identity = identity; + this.secret = secret; + } + + static fromJson(data: ServiceIdentityBasic): IdentityBasic { + return new IdentityBasic(data.identity, data.secret); + } + + toJson(): ServiceIdentityBasic { + return { + type: this.type, + identity: this.identity, + secret: this.secret + }; + } +} + +/** + * Token authentication (API key, static token) + */ +export class IdentityToken extends Identity { + readonly type = 'TA' as const; + token: string; + + constructor(token: string = '') { + super(); + this.token = token; + } + + static fromJson(data: ServiceIdentityToken): IdentityToken { + return new IdentityToken(data.token); + } + + toJson(): ServiceIdentityToken { + return { + type: this.type, + token: this.token + }; + } +} + +/** + * OAuth authentication + */ +export class IdentityOAuth extends Identity { + readonly type = 'OA' as const; + accessToken: string; + accessScope?: string[]; + accessExpiry?: number; + refreshToken?: string; + refreshLocation?: string; + + constructor( + accessToken: string = '', + accessScope?: string[], + accessExpiry?: number, + refreshToken?: string, + refreshLocation?: string + ) { + super(); + this.accessToken = accessToken; + this.accessScope = accessScope; + this.accessExpiry = accessExpiry; + this.refreshToken = refreshToken; + this.refreshLocation = refreshLocation; + } + + static fromJson(data: ServiceIdentityOAuth): IdentityOAuth { + return new IdentityOAuth( + data.accessToken, + data.accessScope, + data.accessExpiry, + data.refreshToken, + data.refreshLocation + ); + } + + toJson(): ServiceIdentityOAuth { + return { + type: this.type, + accessToken: this.accessToken, + ...(this.accessScope && { accessScope: this.accessScope }), + ...(this.accessExpiry && { accessExpiry: this.accessExpiry }), + ...(this.refreshToken && { refreshToken: this.refreshToken }), + ...(this.refreshLocation && { refreshLocation: this.refreshLocation }) + }; + } + + isExpired(): boolean { + if (!this.accessExpiry) return false; + return Date.now() / 1000 >= this.accessExpiry; + } + + expiresIn(): number { + if (!this.accessExpiry) return Infinity; + return Math.max(0, this.accessExpiry - Date.now() / 1000); + } +} + +/** + * Client certificate authentication (mTLS) + */ +export class IdentityCertificate extends Identity { + readonly type = 'CC' as const; + certificate: string; + privateKey: string; + passphrase?: string; + + constructor(certificate: string = '', privateKey: string = '', passphrase?: string) { + super(); + this.certificate = certificate; + this.privateKey = privateKey; + this.passphrase = passphrase; + } + + static fromJson(data: ServiceIdentityCertificate): IdentityCertificate { + return new IdentityCertificate( + data.certificate, + data.privateKey, + data.passphrase + ); + } + + toJson(): ServiceIdentityCertificate { + return { + type: this.type, + certificate: this.certificate, + privateKey: this.privateKey, + ...(this.passphrase && { passphrase: this.passphrase }) + }; + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 9390914..771d251 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,11 +1,7 @@ -/** - * Central export point for all People Manager models - */ - +export { ProviderObject } from './provider'; +export { ServiceObject } from './service'; export { CollectionObject } from './collection'; export { EntityObject } from './entity'; export { GroupObject } from './group'; export { IndividualObject } from './individual'; export { OrganizationObject } from './organization'; -export { ProviderObject } from './provider'; -export { ServiceObject } from './service'; \ No newline at end of file diff --git a/src/models/location.ts b/src/models/location.ts new file mode 100644 index 0000000..7e19cce --- /dev/null +++ b/src/models/location.ts @@ -0,0 +1,240 @@ +/** + * Location implementation classes for Mail Manager services + */ + +import type { + ServiceLocation, + ServiceLocationUri, + ServiceLocationSocketSole, + ServiceLocationSocketSplit, + ServiceLocationFile +} from '@/types/service'; + +/** + * Base Location class + */ +export abstract class Location { + abstract toJson(): ServiceLocation; + + static fromJson(data: ServiceLocation): Location { + switch (data.type) { + case 'URI': + return LocationUri.fromJson(data); + case 'SOCKET_SOLE': + return LocationSocketSole.fromJson(data); + case 'SOCKET_SPLIT': + return LocationSocketSplit.fromJson(data); + case 'FILE': + return LocationFile.fromJson(data); + default: + throw new Error(`Unknown location type: ${(data as any).type}`); + } + } +} + +/** + * URI-based service location for API and web services + * Used by: JMAP, Gmail API, etc. + */ +export class LocationUri extends Location { + readonly type = 'URI' as const; + scheme: string; + host: string; + port: number; + path?: string; + verifyPeer: boolean; + verifyHost: boolean; + + constructor( + scheme: string = 'https', + host: string = '', + port: number = 443, + path?: string, + verifyPeer: boolean = true, + verifyHost: boolean = true + ) { + super(); + this.scheme = scheme; + this.host = host; + this.port = port; + this.path = path; + this.verifyPeer = verifyPeer; + this.verifyHost = verifyHost; + } + + static fromJson(data: ServiceLocationUri): LocationUri { + return new LocationUri( + data.scheme, + data.host, + data.port, + data.path, + data.verifyPeer ?? true, + data.verifyHost ?? true + ); + } + + toJson(): ServiceLocationUri { + return { + type: this.type, + scheme: this.scheme, + host: this.host, + port: this.port, + ...(this.path && { path: this.path }), + ...(this.verifyPeer !== undefined && { verifyPeer: this.verifyPeer }), + ...(this.verifyHost !== undefined && { verifyHost: this.verifyHost }) + }; + } + + getUrl(): string { + const path = this.path || ''; + return `${this.scheme}://${this.host}:${this.port}${path}`; + } +} + +/** + * Single socket-based service location + * Used by: services using a single host/port combination + */ +export class LocationSocketSole extends Location { + readonly type = 'SOCKET_SOLE' as const; + host: string; + port: number; + encryption: 'none' | 'ssl' | 'tls' | 'starttls'; + verifyPeer: boolean; + verifyHost: boolean; + + constructor( + host: string = '', + port: number = 993, + encryption: 'none' | 'ssl' | 'tls' | 'starttls' = 'ssl', + verifyPeer: boolean = true, + verifyHost: boolean = true + ) { + super(); + this.host = host; + this.port = port; + this.encryption = encryption; + this.verifyPeer = verifyPeer; + this.verifyHost = verifyHost; + } + + static fromJson(data: ServiceLocationSocketSole): LocationSocketSole { + return new LocationSocketSole( + data.host, + data.port, + data.encryption, + data.verifyPeer ?? true, + data.verifyHost ?? true + ); + } + + toJson(): ServiceLocationSocketSole { + return { + type: this.type, + host: this.host, + port: this.port, + encryption: this.encryption, + ...(this.verifyPeer !== undefined && { verifyPeer: this.verifyPeer }), + ...(this.verifyHost !== undefined && { verifyHost: this.verifyHost }) + }; + } +} + +/** + * Split socket-based service location + * Used by: traditional IMAP/SMTP configurations + */ +export class LocationSocketSplit extends Location { + readonly type = 'SOCKET_SPLIT' as const; + inboundHost: string; + inboundPort: number; + inboundEncryption: 'none' | 'ssl' | 'tls' | 'starttls'; + outboundHost: string; + outboundPort: number; + outboundEncryption: 'none' | 'ssl' | 'tls' | 'starttls'; + inboundVerifyPeer: boolean; + inboundVerifyHost: boolean; + outboundVerifyPeer: boolean; + outboundVerifyHost: boolean; + + constructor( + inboundHost: string = '', + inboundPort: number = 993, + inboundEncryption: 'none' | 'ssl' | 'tls' | 'starttls' = 'ssl', + outboundHost: string = '', + outboundPort: number = 465, + outboundEncryption: 'none' | 'ssl' | 'tls' | 'starttls' = 'ssl', + inboundVerifyPeer: boolean = true, + inboundVerifyHost: boolean = true, + outboundVerifyPeer: boolean = true, + outboundVerifyHost: boolean = true + ) { + super(); + this.inboundHost = inboundHost; + this.inboundPort = inboundPort; + this.inboundEncryption = inboundEncryption; + this.outboundHost = outboundHost; + this.outboundPort = outboundPort; + this.outboundEncryption = outboundEncryption; + this.inboundVerifyPeer = inboundVerifyPeer; + this.inboundVerifyHost = inboundVerifyHost; + this.outboundVerifyPeer = outboundVerifyPeer; + this.outboundVerifyHost = outboundVerifyHost; + } + + static fromJson(data: ServiceLocationSocketSplit): LocationSocketSplit { + return new LocationSocketSplit( + data.inboundHost, + data.inboundPort, + data.inboundEncryption, + data.outboundHost, + data.outboundPort, + data.outboundEncryption, + data.inboundVerifyPeer ?? true, + data.inboundVerifyHost ?? true, + data.outboundVerifyPeer ?? true, + data.outboundVerifyHost ?? true + ); + } + + toJson(): ServiceLocationSocketSplit { + return { + type: this.type, + inboundHost: this.inboundHost, + inboundPort: this.inboundPort, + inboundEncryption: this.inboundEncryption, + outboundHost: this.outboundHost, + outboundPort: this.outboundPort, + outboundEncryption: this.outboundEncryption, + ...(this.inboundVerifyPeer !== undefined && { inboundVerifyPeer: this.inboundVerifyPeer }), + ...(this.inboundVerifyHost !== undefined && { inboundVerifyHost: this.inboundVerifyHost }), + ...(this.outboundVerifyPeer !== undefined && { outboundVerifyPeer: this.outboundVerifyPeer }), + ...(this.outboundVerifyHost !== undefined && { outboundVerifyHost: this.outboundVerifyHost }) + }; + } +} + +/** + * File-based service location + * Used by: local file system providers + */ +export class LocationFile extends Location { + readonly type = 'FILE' as const; + path: string; + + constructor(path: string = '') { + super(); + this.path = path; + } + + static fromJson(data: ServiceLocationFile): LocationFile { + return new LocationFile(data.path); + } + + toJson(): ServiceLocationFile { + return { + type: this.type, + path: this.path + }; + } +} diff --git a/src/models/provider.ts b/src/models/provider.ts index 9aefd56..2b1b388 100644 --- a/src/models/provider.ts +++ b/src/models/provider.ts @@ -14,7 +14,7 @@ export class ProviderObject implements ProviderInterface { constructor() { this._data = { '@type': 'people:provider', - id: '', + identifier: '', label: '', capabilities: {}, }; @@ -30,9 +30,10 @@ export class ProviderObject implements ProviderInterface { } capable(capability: keyof ProviderCapabilitiesInterface): boolean { - return !!(this._data.capabilities && this._data.capabilities[capability]); - } - + const value = this._data.capabilities?.[capability]; + return value !== undefined && value !== false; + } + capability(capability: keyof ProviderCapabilitiesInterface): any | null { if (this._data.capabilities) { return this._data.capabilities[capability]; @@ -46,8 +47,8 @@ export class ProviderObject implements ProviderInterface { return this._data['@type']; } - get id(): string { - return this._data.id; + get identifier(): string { + return this._data.identifier; } get label(): string { @@ -58,4 +59,4 @@ export class ProviderObject implements ProviderInterface { return this._data.capabilities; } -} \ No newline at end of file +} diff --git a/src/models/service.ts b/src/models/service.ts index c223d8e..3f1934f 100644 --- a/src/models/service.ts +++ b/src/models/service.ts @@ -4,8 +4,12 @@ import type { ServiceInterface, - ServiceCapabilitiesInterface + ServiceCapabilitiesInterface, + ServiceIdentity, + ServiceLocation } from "@/types/service"; +import { Identity } from './identity'; +import { Location } from './location'; export class ServiceObject implements ServiceInterface { @@ -15,10 +19,10 @@ export class ServiceObject implements ServiceInterface { this._data = { '@type': 'people:service', provider: '', - id: '', - label: '', - capabilities: {}, + identifier: null, + label: null, enabled: false, + capabilities: {} }; } @@ -32,7 +36,8 @@ export class ServiceObject implements ServiceInterface { } capable(capability: keyof ServiceCapabilitiesInterface): boolean { - return !!(this._data.capabilities && this._data.capabilities[capability]); + const value = this._data.capabilities?.[capability]; + return value !== undefined && value !== false; } capability(capability: keyof ServiceCapabilitiesInterface): any | null { @@ -52,8 +57,8 @@ export class ServiceObject implements ServiceInterface { return this._data.provider; } - get id(): string { - return this._data.id; + get identifier(): string | number | null { + return this._data.identifier; } get capabilities(): ServiceCapabilitiesInterface | undefined { @@ -62,11 +67,11 @@ export class ServiceObject implements ServiceInterface { /** Mutable Properties */ - get label(): string { + get label(): string | null { return this._data.label; } - set label(value: string) { + set label(value: string | null) { this._data.label = value; } @@ -78,4 +83,46 @@ export class ServiceObject implements ServiceInterface { this._data.enabled = value; } -} \ No newline at end of file + get location(): ServiceLocation | null { + return this._data.location ?? null; + } + + set location(value: ServiceLocation | null) { + this._data.location = value; + } + + get identity(): ServiceIdentity | null { + return this._data.identity ?? null; + } + + set identity(value: ServiceIdentity | null) { + this._data.identity = value; + } + + get auxiliary(): Record { + return this._data.auxiliary ?? {}; + } + + set auxiliary(value: Record) { + this._data.auxiliary = value; + } + + /** Helper Methods */ + + /** + * Get identity as a class instance for easier manipulation + */ + getIdentity(): Identity | null { + if (!this._data.identity) return null; + return Identity.fromJson(this._data.identity); + } + + /** + * Get location as a class instance for easier manipulation + */ + getLocation(): Location | null { + if (!this._data.location) return null; + return Location.fromJson(this._data.location); + } + +} diff --git a/src/services/collectionService.ts b/src/services/collectionService.ts index 01615d8..1ab0fed 100644 --- a/src/services/collectionService.ts +++ b/src/services/collectionService.ts @@ -12,73 +12,120 @@ import type { CollectionFetchResponse, CollectionCreateRequest, CollectionCreateResponse, - CollectionModifyRequest, - CollectionModifyResponse, - CollectionDestroyRequest, - CollectionDestroyResponse, + CollectionUpdateResponse, + CollectionUpdateRequest, + CollectionDeleteResponse, + CollectionDeleteRequest, + CollectionInterface, } from '../types/collection'; +import { useIntegrationStore } from '@KTXC/stores/integrationStore'; +import { CollectionObject, CollectionPropertiesObject } from '../models/collection'; + +/** + * Helper to create the right collection model class based on provider identifier + * Uses provider-specific factory if available, otherwise returns base CollectionObject + */ +function createCollectionObject(data: CollectionInterface): CollectionObject { + const integrationStore = useIntegrationStore(); + const factoryItem = integrationStore.getItemById('people_collection_factory', data.provider) as any; + const factory = factoryItem?.factory; + + // Use provider factory if available, otherwise base class + return factory ? factory(data) : new CollectionObject().fromJson(data); +} export const collectionService = { /** - * List all available collections + * Retrieve list of collections, optionally filtered by source selector * - * @param request - Collection list request parameters - * @returns Promise with collection list grouped by provider and service + * @param request - list request parameters + * + * @returns Promise with collection object list grouped by provider, service, and collection identifier */ - async list(request: CollectionListRequest = {}): Promise { - return await transceivePost('collection.list', request); + async list(request: CollectionListRequest = {}): Promise>>> { + const response = await transceivePost('collection.list', request); + + // Convert nested response to CollectionObject instances + const providerList: Record>> = {}; + Object.entries(response).forEach(([providerId, providerServices]) => { + const serviceList: Record> = {}; + Object.entries(providerServices).forEach(([serviceId, serviceCollections]) => { + const collectionList: Record = {}; + Object.entries(serviceCollections).forEach(([collectionId, collectionData]) => { + collectionList[collectionId] = createCollectionObject(collectionData); + }); + serviceList[serviceId] = collectionList; + }); + providerList[providerId] = serviceList; + }); + + return providerList; }, /** - * Check which collections exist/are available + * Retrieve a specific collection by provider and identifier + * + * @param request - fetch request parameters + * + * @returns Promise with collection object + */ + async fetch(request: CollectionFetchRequest): Promise { + const response = await transceivePost('collection.fetch', request); + return createCollectionObject(response); + }, + + /** + * Retrieve collection availability status for a given source selector + * + * @param request - extant request parameters * - * @param request - Collection extant request with source selector * @returns Promise with collection availability status */ async extant(request: CollectionExtantRequest): Promise { - return await transceivePost('collection.extant', request); - }, - - /** - * Fetch a specific collection - * - * @param request - Collection fetch request - * @returns Promise with collection details - */ - async fetch(request: CollectionFetchRequest): Promise { - return await transceivePost('collection.fetch', request); + return await transceivePost('collection.extant', request); }, /** * Create a new collection * - * @param request - Collection create request - * @returns Promise with created collection + * @param request - create request parameters + * + * @returns Promise with created collection object */ - async create(request: CollectionCreateRequest): Promise { - return await transceivePost('collection.create', request); + async create(request: CollectionCreateRequest): Promise { + if (request.properties instanceof CollectionPropertiesObject) { + request.properties = request.properties.toJson(); + } + const response = await transceivePost('collection.create', request); + return createCollectionObject(response); }, /** - * Modify an existing collection + * Update an existing collection * - * @param request - Collection modify request - * @returns Promise with modified collection + * @param request - update request parameters + * + * @returns Promise with updated collection object */ - async modify(request: CollectionModifyRequest): Promise { - return await transceivePost('collection.modify', request); + async update(request: CollectionUpdateRequest): Promise { + if (request.properties instanceof CollectionPropertiesObject) { + request.properties = request.properties.toJson(); + } + const response = await transceivePost('collection.update', request); + return createCollectionObject(response); }, /** * Delete a collection * - * @param request - Collection destroy request + * @param request - delete request parameters + * * @returns Promise with deletion result */ - async destroy(request: CollectionDestroyRequest): Promise { - return await transceivePost('collection.destroy', request); + async delete(request: CollectionDeleteRequest): Promise { + return await transceivePost('collection.delete', request); }, }; -export default collectionService; \ No newline at end of file +export default collectionService; diff --git a/src/services/entityService.ts b/src/services/entityService.ts index e9bd89d..92d75dc 100644 --- a/src/services/entityService.ts +++ b/src/services/entityService.ts @@ -6,91 +6,145 @@ import { transceivePost } from './transceive'; import type { EntityListRequest, EntityListResponse, - EntityDeltaRequest, - EntityDeltaResponse, - EntityExtantRequest, - EntityExtantResponse, EntityFetchRequest, EntityFetchResponse, + EntityExtantRequest, + EntityExtantResponse, EntityCreateRequest, EntityCreateResponse, - EntityModifyRequest, - EntityModifyResponse, - EntityDestroyRequest, - EntityDestroyResponse, + EntityUpdateRequest, + EntityUpdateResponse, + EntityDeleteRequest, + EntityDeleteResponse, + EntityDeltaRequest, + EntityDeltaResponse, + EntityInterface, } from '../types/entity'; +import { useIntegrationStore } from '@KTXC/stores/integrationStore'; +import { EntityObject } from '../models'; + +/** + * Helper to create the right entity model class based on provider identifier + * Uses provider-specific factory if available, otherwise returns base EntityObject + */ +function createEntityObject(data: EntityInterface): EntityObject { + const integrationStore = useIntegrationStore(); + const factoryItem = integrationStore.getItemById('people_entity_factory', data.provider) as any; + const factory = factoryItem?.factory; + + // Use provider factory if available, otherwise base class + return factory ? factory(data) : new EntityObject().fromJson(data); +} export const entityService = { /** - * List all available entities (events, tasks, journals) + * Retrieve list of entities, optionally filtered by source selector * - * @param request - Entity list request parameters - * @returns Promise with entity list grouped by provider, service, and collection + * @param request - list request parameters + * + * @returns Promise with entity object list grouped by provider, service, collection, and entity identifier */ - async list(request: EntityListRequest = {}): Promise { - return await transceivePost('entity.list', request); + async list(request: EntityListRequest = {}): Promise>>>> { + const response = await transceivePost('entity.list', request); + + // Convert nested response to EntityObject instances + const providerList: Record>>> = {}; + Object.entries(response).forEach(([providerId, providerServices]) => { + const serviceList: Record>> = {}; + Object.entries(providerServices).forEach(([serviceId, serviceCollections]) => { + const collectionList: Record> = {}; + Object.entries(serviceCollections).forEach(([collectionId, collectionEntities]) => { + const entityList: Record = {}; + Object.entries(collectionEntities).forEach(([entityId, entityData]) => { + entityList[entityId] = createEntityObject(entityData); + }); + collectionList[collectionId] = entityList; + }); + serviceList[serviceId] = collectionList; + }); + providerList[providerId] = serviceList; + }); + + return providerList; }, /** - * Get delta changes for entities + * Retrieve a specific entity by provider and identifier * - * @param request - Entity delta request with source selector - * @returns Promise with delta changes (created, modified, deleted) + * @param request - fetch request parameters + * + * @returns Promise with entity objects keyed by identifier */ - async delta(request: EntityDeltaRequest): Promise { - return await transceivePost('entity.delta', request); + async fetch(request: EntityFetchRequest): Promise> { + const response = await transceivePost('entity.fetch', request); + + // Convert response to EntityObject instances + const list: Record = {}; + Object.entries(response).forEach(([identifier, entityData]) => { + list[identifier] = createEntityObject(entityData); + }); + + return list; }, /** - * Check which entities exist/are available + * Retrieve entity availability status for a given source selector + * + * @param request - extant request parameters * - * @param request - Entity extant request with source selector * @returns Promise with entity availability status */ async extant(request: EntityExtantRequest): Promise { - return await transceivePost('entity.extant', request); - }, - - /** - * Fetch specific entities - * - * @param request - Entity fetch request - * @returns Promise with entity details - */ - async fetch(request: EntityFetchRequest): Promise { - return await transceivePost('entity.fetch', request); + return await transceivePost('entity.extant', request); }, /** * Create a new entity * - * @param request - Entity create request - * @returns Promise with created entity + * @param request - create request parameters + * + * @returns Promise with created entity object */ - async create(request: EntityCreateRequest): Promise { - return await transceivePost('entity.create', request); + async create(request: EntityCreateRequest): Promise { + const response = await transceivePost('entity.create', request); + return createEntityObject(response); }, /** - * Modify an existing entity + * Update an existing entity * - * @param request - Entity modify request - * @returns Promise with modified entity + * @param request - update request parameters + * + * @returns Promise with updated entity object */ - async modify(request: EntityModifyRequest): Promise { - return await transceivePost('entity.modify', request); + async update(request: EntityUpdateRequest): Promise { + const response = await transceivePost('entity.update', request); + return createEntityObject(response); }, /** * Delete an entity * - * @param request - Entity destroy request + * @param request - delete request parameters + * * @returns Promise with deletion result */ - async destroy(request: EntityDestroyRequest): Promise { - return await transceivePost('entity.destroy', request); + async delete(request: EntityDeleteRequest): Promise { + return await transceivePost('entity.delete', request); }, + + /** + * Retrieve delta changes for entities + * + * @param request - delta request parameters + * + * @returns Promise with delta changes (created, modified, deleted) + */ + async delta(request: EntityDeltaRequest): Promise { + return await transceivePost('entity.delta', request); + }, + }; export default entityService; diff --git a/src/services/index.ts b/src/services/index.ts index 60793c5..66fbd58 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,16 +1,4 @@ -/** - * Central export point for all People Manager services - */ - -// Services export { providerService } from './providerService'; export { serviceService } from './serviceService'; export { collectionService } from './collectionService'; export { entityService } from './entityService'; - -// Type exports -export type * from '../types/common'; -export type * from '../types/provider'; -export type * from '../types/service'; -export type * from '../types/collection'; -export type * from '../types/entity'; diff --git a/src/services/providerService.ts b/src/services/providerService.ts index 4642322..1a4277e 100644 --- a/src/services/providerService.ts +++ b/src/services/providerService.ts @@ -2,30 +2,74 @@ * Provider management service */ +import type { + ProviderListRequest, + ProviderListResponse, + ProviderExtantRequest, + ProviderExtantResponse, + ProviderFetchRequest, + ProviderFetchResponse, + ProviderInterface, +} from '../types/provider'; +import { useIntegrationStore } from '@KTXC/stores/integrationStore'; import { transceivePost } from './transceive'; -import type { ProviderListResponse, ProviderExtantResponse } from '../types/provider'; -import type { SourceSelector } from '../types/common'; +import { ProviderObject } from '../models/provider'; + +/** + * Helper to create the right provider model class based on provider identifier + * Uses provider-specific factory if available, otherwise returns base ProviderObject + */ +function createProviderObject(data: ProviderInterface): ProviderObject { + const integrationStore = useIntegrationStore(); + const factoryItem = integrationStore.getItemById('people_provider_factory', data.identifier) as any; + const factory = factoryItem?.factory; + + // Use provider factory if available, otherwise base class + return factory ? factory(data) : new ProviderObject().fromJson(data); +} export const providerService = { /** - * List all available providers + * Retrieve list of providers, optionally filtered by source selector * - * @returns Promise with provider list keyed by provider ID + * @param request - list request parameters + * + * @returns Promise with provider object list keyed by provider identifier */ - async list(): Promise { - return await transceivePost<{}, ProviderListResponse>('provider.list', {}); + async list(request: ProviderListRequest = {}): Promise> { + const response = await transceivePost('provider.list', request); + + // Convert response to ProviderObject instances + const list: Record = {}; + Object.entries(response).forEach(([providerId, providerData]) => { + list[providerId] = createProviderObject(providerData); + }); + + return list; }, /** - * Check which providers exist/are available + * Retrieve specific provider by identifier * - * @param sources - Source selector with provider IDs to check + * @param request - fetch request parameters + * + * @returns Promise with provider object + */ + async fetch(request: ProviderFetchRequest): Promise { + const response = await transceivePost('provider.fetch', request); + return createProviderObject(response); + }, + + /** + * Retrieve provider availability status for a given source selector + * + * @param request - extant request parameters * * @returns Promise with provider availability status */ - async extant(sources: SourceSelector): Promise { - return await transceivePost('provider.extant', { sources }); + async extant(request: ProviderExtantRequest): Promise { + return await transceivePost('provider.extant', request); }, }; diff --git a/src/services/serviceService.ts b/src/services/serviceService.ts index 4b64b31..6e39dba 100644 --- a/src/services/serviceService.ts +++ b/src/services/serviceService.ts @@ -2,46 +2,161 @@ * Service management service */ -import { transceivePost } from './transceive'; import type { ServiceListRequest, ServiceListResponse, - ServiceExtantRequest, - ServiceExtantResponse, ServiceFetchRequest, ServiceFetchResponse, + ServiceExtantRequest, + ServiceExtantResponse, + ServiceCreateResponse, + ServiceCreateRequest, + ServiceUpdateResponse, + ServiceUpdateRequest, + ServiceDeleteResponse, + ServiceDeleteRequest, + ServiceDiscoverRequest, + ServiceDiscoverResponse, + ServiceTestRequest, + ServiceTestResponse, + ServiceInterface, } from '../types/service'; +import { useIntegrationStore } from '@KTXC/stores/integrationStore'; +import { transceivePost } from './transceive'; +import { ServiceObject } from '../models/service'; + +/** + * Helper to create the right service model class based on provider identifier + * Uses provider-specific factory if available, otherwise returns base ServiceObject + */ +function createServiceObject(data: ServiceInterface): ServiceObject { + const integrationStore = useIntegrationStore(); + const factoryItem = integrationStore.getItemById('people_service_factory', data.provider) as any; + const factory = factoryItem?.factory; + + // Use provider factory if available, otherwise base class + return factory ? factory(data) : new ServiceObject().fromJson(data); +} export const serviceService = { /** - * List all available services + * Retrieve list of services, optionally filtered by source selector * - * @param request - Service list request parameters - * @returns Promise with service list grouped by provider + * @param request - list request parameters + * + * @returns Promise with service object list grouped by provider and keyed by service identifier */ - async list(request: ServiceListRequest = {}): Promise { - return await transceivePost('service.list', request); + async list(request: ServiceListRequest = {}): Promise>> { + const response = await transceivePost('service.list', request); + + // Convert nested response to ServiceObject instances + const providerList: Record> = {}; + Object.entries(response).forEach(([providerId, providerServices]) => { + const serviceList: Record = {}; + Object.entries(providerServices).forEach(([serviceId, serviceData]) => { + serviceList[serviceId] = createServiceObject(serviceData); + }); + providerList[providerId] = serviceList; + }); + + return providerList; }, /** - * Check which services exist/are available + * Retrieve a specific service by provider and identifier + * + * @param request - fetch request parameters + * + * @returns Promise with service object + */ + async fetch(request: ServiceFetchRequest): Promise { + const response = await transceivePost('service.fetch', request); + return createServiceObject(response); + }, + + /** + * Retrieve service availability status for a given source selector + * + * @param request - extant request parameters * - * @param request - Service extant request with source selector * @returns Promise with service availability status */ async extant(request: ServiceExtantRequest): Promise { - return await transceivePost('service.extant', request); + return await transceivePost('service.extant', request); }, /** - * Fetch a specific service + * Retrieve discoverable services for a given source selector, sorted by provider * - * @param request - Service fetch request with provider and service IDs - * @returns Promise with service details + * @param request - discover request parameters + * + * @returns Promise with array of discovered services sorted by provider */ - async fetch(request: ServiceFetchRequest): Promise { - return await transceivePost('service.fetch', request); + async discover(request: ServiceDiscoverRequest): Promise { + const response = await transceivePost('service.discover', request); + + // Convert discovery results to ServiceObjects + const services: ServiceObject[] = []; + Object.entries(response).forEach(([providerId, location]) => { + const serviceData: ServiceInterface = { + '@type': 'people:service', + provider: providerId, + identifier: null, + label: null, + enabled: false, + location: location, + }; + services.push(createServiceObject(serviceData)); + }); + + // Sort by provider + return services.sort((a, b) => a.provider.localeCompare(b.provider)); + }, + + /** + * Test service connectivity and configuration + * + * @param request - Service test request + * @returns Promise with test results + */ + async test(request: ServiceTestRequest): Promise { + return await transceivePost('service.test', request); + }, + + /** + * Create a new service + * + * @param request - create request parameters + * + * @returns Promise with created service object + */ + async create(request: ServiceCreateRequest): Promise { + const response = await transceivePost('service.create', request); + return createServiceObject(response); + }, + + /** + * Update a existing service + * + * @param request - update request parameters + * + * @returns Promise with updated service object + */ + async update(request: ServiceUpdateRequest): Promise { + const response = await transceivePost('service.update', request); + return createServiceObject(response); + }, + + /** + * Delete a service + * + * @param request - delete request parameters + * + * @returns Promise with deletion result + */ + async delete(request: { provider: string; identifier: string | number }): Promise { + return await transceivePost('service.delete', request); }, }; diff --git a/src/stores/collectionsStore.ts b/src/stores/collectionsStore.ts index 41312f0..d39e9a2 100644 --- a/src/stores/collectionsStore.ts +++ b/src/stores/collectionsStore.ts @@ -1,203 +1,307 @@ /** - * People Manager - Collections Store + * Collections Store */ -import { defineStore } from 'pinia'; -import { ref } from 'vue'; -import { collectionService } from '../services/collectionService'; -import type { - SourceSelector, - ListFilter, - ListSort, -} from '../types/common'; -import { CollectionObject } from '../models/collection'; -import type { ServiceObject } from '../models/service'; -import type { CollectionInterface } from '../types/collection'; +import { ref, computed, readonly } from 'vue' +import { defineStore } from 'pinia' +import { collectionService } from '../services' +import { CollectionObject, CollectionPropertiesObject } from '../models/collection' +import type { SourceSelector, ListFilter, ListSort } from '../types' -export const useCollectionsStore = defineStore('collectionsStore', () => { +export const useCollectionsStore = defineStore('peopleCollectionsStore', () => { // State - const collections = ref([]); - - // Actions + const _collections = ref>({}) + const transceiving = ref(false) /** - * Retrieve collections from the server + * Get count of collections in store */ - async function list( - sources?: SourceSelector, - filter?: ListFilter, - sort?: ListSort, - uid?: string - ): Promise { - try { - const response = await collectionService.list({ sources, filter, sort, uid }); - - // Flatten the nested response into a flat array - const flatCollections: CollectionObject[] = []; - Object.entries(response).forEach(([_providerId, providerCollections]) => { - Object.entries(providerCollections).forEach(([_serviceId, serviceCollections]) => { - Object.values(serviceCollections).forEach((collection: CollectionInterface) => { - flatCollections.push(new CollectionObject().fromJson(collection)); - }); - }); - }); - - console.debug('[People Manager](Store) - Successfully retrieved', flatCollections.length, 'collections:', flatCollections.map(c => ({ - id: c.id, - label: c.label, - service: c.service, - provider: c.provider - }))); - - collections.value = flatCollections; - return flatCollections; - } catch (error: any) { - console.error('[People Manager](Store) - Failed to retrieve collections:', error); - throw error; - } - } + const count = computed(() => Object.keys(_collections.value).length) /** - * Fetch a specific collection + * Check if any collections are present in store */ - async function fetch( - provider: string, - service: string, - identifier: string | number, - uid?: string - ): Promise { - try { - const response = await collectionService.fetch({ provider, service, identifier, uid }); - - return new CollectionObject().fromJson(response); - } catch (error: any) { - console.error('[People Manager](Store) - Failed to fetch collection:', error); - throw error; - } - } + const has = computed(() => count.value > 0) /** - * Create a fresh collection object with default values + * Get all collections present in store */ - function fresh(): CollectionObject { - return new CollectionObject(); - } + const collections = computed(() => Object.values(_collections.value)) /** - * Create a new collection + * Get all collections present in store grouped by service */ - async function create( - service: ServiceObject, - collection: CollectionObject, - options?: string[], - uid?: string - ): Promise { - try { - if (service.provider === null || service.id === null) { - throw new Error('Invalid service object, must have a provider and identifier'); + const collectionsByService = computed(() => { + const groups: Record = {} + + Object.values(_collections.value).forEach((collection) => { + const serviceKey = `${collection.provider}:${collection.service}` + if (!groups[serviceKey]) { + groups[serviceKey] = [] } + groups[serviceKey].push(collection) + }) + + return groups + }) - const response = await collectionService.create({ - provider: service.provider, - service: service.id, - data: collection.toJson(), - options, - uid - }); - - const createdCollection = new CollectionObject().fromJson(response); - collections.value.push(createdCollection); - - console.debug('[People Manager](Store) - Successfully created collection'); - - return createdCollection; - } catch (error: any) { - console.error('[People Manager](Store) - Failed to create collection:', error); - throw error; + /** + * Get a specific collection from store, with optional retrieval + * + * @param provider - provider identifier + * @param service - service identifier + * @param identifier - collection identifier + * @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only + * + * @returns Collection object or null + */ + function collection(provider: string, service: string | number, identifier: string | number, retrieve: boolean = false): CollectionObject | null { + const key = identifierKey(provider, service, identifier) + if (retrieve === true && !_collections.value[key]) { + console.debug(`[People Manager][Store] - Force fetching collection "${key}"`) + fetch(provider, service, identifier) } + + return _collections.value[key] || null } /** - * Modify an existing collection + * Get all collections for a specific service + * + * @param provider - provider identifier + * @param service - service identifier + * @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only + * + * @returns Array of collection objects */ - async function modify( - collection: CollectionObject, - uid?: string - ): Promise { - try { - if (!collection.provider || !collection.service || !collection.id) { - throw new Error('Collection must have provider, service, and id'); - } - - const response = await collectionService.modify({ - provider: collection.provider, - service: collection.service, - identifier: collection.id, - data: collection.toJson(), - uid - }); - - const modifiedCollection = new CollectionObject().fromJson(response); - const index = collections.value.findIndex(c => c.id === collection.id); - if (index !== -1) { - collections.value[index] = modifiedCollection; - } - - console.debug('[People Manager](Store) - Successfully modified collection'); - - return modifiedCollection; - } catch (error: any) { - console.error('[People Manager](Store) - Failed to modify collection:', error); - throw error; - } - } - -/** - * Delete a collection - */ - async function destroy( - collection: CollectionObject, - uid?: string - ): Promise { - try { - if (!collection.provider || !collection.service || !collection.id) { - throw new Error('Collection must have provider, service, and id'); - } - - const response = await collectionService.destroy({ - provider: collection.provider, - service: collection.service, - identifier: collection.id, - uid - }); - - if (response.success) { - const index = collections.value.findIndex(c => c.id === collection.id); - if (index !== -1) { - collections.value.splice(index, 1); + function collectionsForService(provider: string, service: string | number, retrieve: boolean = false): CollectionObject[] { + const serviceKeyPrefix = `${provider}:${service}:` + const serviceCollections = Object.entries(_collections.value) + .filter(([key]) => key.startsWith(serviceKeyPrefix)) + .map(([_, collection]) => collection) + + if (retrieve === true && serviceCollections.length === 0) { + console.debug(`[People Manager][Store] - Force fetching collections for service "${provider}:${service}"`) + const sources: SourceSelector = { + [provider]: { + [String(service)]: true } } - - console.debug('[People Manager](Store) - Successfully destroyed collection'); - - return response.success; + list(sources) + } + + return serviceCollections + } + + /** + * Create unique key for a collection + */ + function identifierKey(provider: string, service: string | number | null, identifier: string | number | null): string { + return `${provider}:${service ?? ''}:${identifier ?? ''}` + } + + // Actions + + /** + * Retrieve all or specific collections, optionally filtered by source selector + * + * @param sources - optional source selector + * @param filter - optional list filter + * @param sort - optional list sort + * + * @returns Promise with collection object list keyed by provider, service, and collection identifier + */ + async function list(sources?: SourceSelector, filter?: ListFilter, sort?: ListSort): Promise> { + transceiving.value = true + try { + const response = await collectionService.list({ sources, filter, sort }) + + // Flatten nested structure: provider:service:collection -> "provider:service:collection": object + const collections: Record = {} + Object.entries(response).forEach(([_providerId, providerServices]) => { + Object.entries(providerServices).forEach(([_serviceId, serviceCollections]) => { + Object.entries(serviceCollections).forEach(([_collectionId, collectionObj]) => { + const key = identifierKey(collectionObj.provider, collectionObj.service, collectionObj.identifier) + collections[key] = collectionObj + }) + }) + }) + + // Merge retrieved collections into state + _collections.value = { ..._collections.value, ...collections } + + console.debug('[People Manager][Store] - Successfully retrieved', Object.keys(collections).length, 'collections') + return collections } catch (error: any) { - console.error('[People Manager](Store) - Failed to destroy collection:', error); - throw error; + console.error('[People Manager][Store] - Failed to retrieve collections:', error) + throw error + } finally { + transceiving.value = false + } + } + + /** + * Retrieve a specific collection by provider, service, and identifier + * + * @param provider - provider identifier + * @param service - service identifier + * @param identifier - collection identifier + * + * @returns Promise with collection object + */ + async function fetch(provider: string, service: string | number, identifier: string | number): Promise { + transceiving.value = true + try { + const response = await collectionService.fetch({ provider, service, collection: identifier }) + + // Merge fetched collection into state + const key = identifierKey(response.provider, response.service, response.identifier) + _collections.value[key] = response + + console.debug('[People Manager][Store] - Successfully fetched collection:', key) + return response + } catch (error: any) { + console.error('[People Manager][Store] - Failed to fetch collection:', error) + throw error + } finally { + transceiving.value = false } } + /** + * Retrieve collection availability status for a given source selector + * + * @param sources - source selector to check availability for + * + * @returns Promise with collection availability status + */ + async function extant(sources: SourceSelector) { + transceiving.value = true + try { + const response = await collectionService.extant({ sources }) + + console.debug('[People Manager][Store] - Successfully checked', sources ? Object.keys(sources).length : 0, 'collections') + return response + } catch (error: any) { + console.error('[People Manager][Store] - Failed to check collections:', error) + throw error + } finally { + transceiving.value = false + } + } + /** + * Create a new collection with given provider, service, and data + * + * @param provider - provider identifier for the new collection + * @param service - service identifier for the new collection + * @param collection - optional parent collection identifier + * @param data - collection properties for creation + * + * @returns Promise with created collection object + */ + async function create(provider: string, service: string | number, collection: string | number | null, data: CollectionPropertiesObject): Promise { + transceiving.value = true + try { + const response = await collectionService.create({ + provider, + service, + collection, + properties: data + }) + + // Merge created collection into state + const key = identifierKey(response.provider, response.service, response.identifier) + _collections.value[key] = response + + console.debug('[People Manager][Store] - Successfully created collection:', key) + return response + } catch (error: any) { + console.error('[People Manager][Store] - Failed to create collection:', error) + throw error + } finally { + transceiving.value = false + } + } + + /** + * Update an existing collection with given provider, service, identifier, and data + * + * @param provider - provider identifier for the collection to update + * @param service - service identifier for the collection to update + * @param identifier - collection identifier for the collection to update + * @param data - collection properties for update + * + * @returns Promise with updated collection object + */ + async function update(provider: string, service: string | number, identifier: string | number, data: CollectionPropertiesObject): Promise { + transceiving.value = true + try { + const response = await collectionService.update({ + provider, + service, + identifier, + properties: data + }) + + // Merge updated collection into state + const key = identifierKey(response.provider, response.service, response.identifier) + _collections.value[key] = response + + console.debug('[People Manager][Store] - Successfully updated collection:', key) + return response + } catch (error: any) { + console.error('[People Manager][Store] - Failed to update collection:', error) + throw error + } finally { + transceiving.value = false + } + } + + /** + * Delete a collection by provider, service, and identifier + * + * @param provider - provider identifier for the collection to delete + * @param service - service identifier for the collection to delete + * @param identifier - collection identifier for the collection to delete + * + * @returns Promise with deletion result + */ + async function remove(provider: string, service: string | number, identifier: string | number): Promise { + transceiving.value = true + try { + await collectionService.delete({ provider, service, identifier }) + + // Remove deleted collection from state + const key = identifierKey(provider, service, identifier) + delete _collections.value[key] + + console.debug('[People Manager][Store] - Successfully deleted collection:', key) + } catch (error: any) { + console.error('[People Manager][Store] - Failed to delete collection:', error) + throw error + } finally { + transceiving.value = false + } + } + + // Return public API return { - // State + // State (readonly) + transceiving: readonly(transceiving), + // Getters + count, + has, collections, - + collectionsByService, + collectionsForService, // Actions + collection, list, fetch, - fresh, + extant, create, - modify, - destroy, - }; -}); + update, + delete: remove, + } +}) diff --git a/src/stores/entitiesStore.ts b/src/stores/entitiesStore.ts index 90c713a..17cbc10 100644 --- a/src/stores/entitiesStore.ts +++ b/src/stores/entitiesStore.ts @@ -1,276 +1,346 @@ /** - * People Manager - Entities Store + * Entities Store */ -import { defineStore } from 'pinia'; -import { ref } from 'vue'; -import { entityService } from '../services/entityService'; -import type { - SourceSelector, - ListFilter, - ListSort, - ListRange, -} from '../types/common'; -import type { - EntityInterface, -} from '../types/entity'; -import type { CollectionObject } from '../models/collection'; -import { EntityObject } from '../models/entity' -import { IndividualObject } from '../models/individual'; -import { OrganizationObject } from '../models/organization'; -import { GroupObject } from '../models/group'; +import { ref, computed, readonly } from 'vue' +import { defineStore } from 'pinia' +import { entityService } from '../services' +import { EntityObject } from '../models' +import type { SourceSelector, ListFilter, ListSort, ListRange } from '../types/common' export const useEntitiesStore = defineStore('peopleEntitiesStore', () => { // State - const entities = ref([]); + const _entities = ref>({}) + const transceiving = ref(false) + + /** + * Get count of entities in store + */ + const count = computed(() => Object.keys(_entities.value).length) + + /** + * Check if any entities are present in store + */ + const has = computed(() => count.value > 0) + + /** + * Get all entities present in store + */ + const entities = computed(() => Object.values(_entities.value)) + + /** + * Get a specific entity from store, with optional retrieval + * + * @param provider - provider identifier + * @param service - service identifier + * @param collection - collection identifier + * @param identifier - entity identifier + * @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only + * + * @returns Entity object or null + */ + function entity(provider: string, service: string | number, collection: string | number, identifier: string | number, retrieve: boolean = false): EntityObject | null { + const key = identifierKey(provider, service, collection, identifier) + if (retrieve === true && !_entities.value[key]) { + console.debug(`[People Manager][Store] - Force fetching entity "${key}"`) + fetch(provider, service, collection, [identifier]) + } + + return _entities.value[key] || null + } + + /** + * Get all entities for a specific collection + * + * @param provider - provider identifier + * @param service - service identifier + * @param collection - collection identifier + * @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only + * + * @returns Array of entity objects + */ + function entitiesForCollection(provider: string, service: string | number, collection: string | number, retrieve: boolean = false): EntityObject[] { + const collectionKeyPrefix = `${provider}:${service}:${collection}:` + const collectionEntities = Object.entries(_entities.value) + .filter(([key]) => key.startsWith(collectionKeyPrefix)) + .map(([_, entity]) => entity) + + if (retrieve === true && collectionEntities.length === 0) { + console.debug(`[People Manager][Store] - Force fetching entities for collection "${provider}:${service}:${collection}"`) + const sources: SourceSelector = { + [provider]: { + [String(service)]: { + [String(collection)]: true + } + } + } + list(sources) + } + + return collectionEntities + } + + /** + * Create unique key for an entity + */ + function identifierKey(provider: string, service: string | number, collection: string | number, identifier: string | number): string { + return `${provider}:${service}:${collection}:${identifier}` + } // Actions /** - * Reset the store to initial state + * Retrieve all or specific entities, optionally filtered by source selector + * + * @param sources - optional source selector + * @param filter - optional list filter + * @param sort - optional list sort + * @param range - optional list range + * + * @returns Promise with entity object list keyed by identifier */ - function reset(): void { - entities.value = []; - } -/** - * List entities for all or specific collection - */ - async function list( - provider: string | null, - service: string | null, - collection: string | number | null, - filter?: ListFilter, - sort?: ListSort, - range?: ListRange, - uid?: string - ): Promise { + async function list(sources?: SourceSelector, filter?: ListFilter, sort?: ListSort, range?: ListRange): Promise> { + transceiving.value = true try { - // Validate hierarchical requirements - if (collection !== null && (service === null || provider === null)) { - throw new Error('Collection requires both service and provider'); - } - if (service !== null && provider === null) { - throw new Error('Service requires provider'); - } + const response = await entityService.list({ sources, filter, sort, range }) - // Build sources object level by level - const sources: SourceSelector = {}; - if (provider !== null) { - if (service !== null) { - if (collection !== null) { - sources[provider] = { [service]: { [collection]: true } }; - } else { - sources[provider] = { [service]: true }; - } - } else { - sources[provider] = true; - } - } + // Flatten nested structure: provider:service:collection:entity -> "provider:service:collection:entity": object + const entities: Record = {} + Object.entries(response).forEach(([providerId, providerServices]) => { + Object.entries(providerServices).forEach(([serviceId, serviceCollections]) => { + Object.entries(serviceCollections).forEach(([collectionId, collectionEntities]) => { + Object.entries(collectionEntities).forEach(([entityId, entityData]) => { + const key = identifierKey(providerId, serviceId, collectionId, entityId) + entities[key] = entityData + }) + }) + }) + }) - // Transmit - const response = await entityService.list({ sources, filter, sort, range, uid }); - - // Flatten the nested response into a flat array - const flatEntities: EntityObject[] = []; - Object.entries(response).forEach(([, providerEntities]) => { - Object.entries(providerEntities).forEach(([, serviceEntities]) => { - Object.entries(serviceEntities).forEach(([, collectionEntities]) => { - Object.values(collectionEntities).forEach((entity: EntityInterface) => { - flatEntities.push(new EntityObject().fromJson(entity)); - }); - }); - }); - }); - - console.debug('[People Manager](Store) - Successfully retrieved', flatEntities.length, 'entities'); - - entities.value = flatEntities; - return flatEntities; + // Merge retrieved entities into state + _entities.value = { ..._entities.value, ...entities } + + console.debug('[People Manager][Store] - Successfully retrieved', Object.keys(entities).length, 'entities') + return entities } catch (error: any) { - console.error('[People Manager](Store) - Failed to retrieve entities:', error); - throw error; + console.error('[People Manager][Store] - Failed to retrieve entities:', error) + throw error + } finally { + transceiving.value = false + } + } + + /** + * Retrieve specific entities by provider, service, collection, and identifiers + * + * @param provider - provider identifier + * @param service - service identifier + * @param collection - collection identifier + * @param identifiers - array of entity identifiers to fetch + * + * @returns Promise with entity objects keyed by identifier + */ + async function fetch(provider: string, service: string | number, collection: string | number, identifiers: (string | number)[]): Promise> { + transceiving.value = true + try { + const response = await entityService.fetch({ provider, service, collection, identifiers }) + + // Merge fetched entities into state + const entities: Record = {} + Object.entries(response).forEach(([identifier, entityData]) => { + const key = identifierKey(provider, service, collection, identifier) + entities[key] = entityData + _entities.value[key] = entityData + }) + + console.debug('[People Manager][Store] - Successfully fetched', Object.keys(entities).length, 'entities') + return entities + } catch (error: any) { + console.error('[People Manager][Store] - Failed to fetch entities:', error) + throw error + } finally { + transceiving.value = false } } /** - * Fetch entities for a specific collection + * Retrieve entity availability status for a given source selector + * + * @param sources - source selector to check availability for + * + * @returns Promise with entity availability status */ - async function fetch( - collection: CollectionObject, - identifiers: (string | number)[], - uid?: string - ): Promise { + async function extant(sources: SourceSelector) { + transceiving.value = true try { - if (!collection.provider || !collection.service || !collection.id) { - throw new Error('Collection must have provider, service, and id'); - } - - const response = await entityService.fetch({ - provider: collection.provider, - service: collection.service, - collection: collection.id, - identifiers, - uid - }); - - return Object.values(response).map(entity => new EntityObject().fromJson(entity)); + const response = await entityService.extant({ sources }) + console.debug('[People Manager][Store] - Successfully checked entity availability') + return response } catch (error: any) { - console.error('[People Manager](Store) - Failed to fetch entities:', error); - throw error; + console.error('[People Manager][Store] - Failed to check entity availability:', error) + throw error + } finally { + transceiving.value = false } } /** - * Create a fresh entity object + * Create a new entity with given provider, service, collection, and data + * + * @param provider - provider identifier for the new entity + * @param service - service identifier for the new entity + * @param collection - collection identifier for the new entity + * @param data - entity properties for creation + * + * @returns Promise with created entity object */ - function fresh(type: string): EntityObject { - const entity = new EntityObject(); - - if (type === 'organization') { - entity.data = new OrganizationObject(); - } else if (type === 'group') { - entity.data = new GroupObject(); - } else { - entity.data = new IndividualObject(); - } - - entity.data.created = new Date(); - - return entity; - } - - /** - * Create a new entity - */ - async function create( - collection: CollectionObject, - entity: EntityObject, - options?: string[], - uid?: string - ): Promise { + async function create(provider: string, service: string | number, collection: string | number, data: any): Promise { + transceiving.value = true try { - if (!collection.provider || !collection.service || !collection.id) { - throw new Error('Collection must have provider, service, and id'); - } + const response = await entityService.create({ provider, service, collection, properties: data }) + + // Add created entity to state + const key = identifierKey(response.provider, response.service, response.collection, response.identifier) + _entities.value[key] = response - const response = await entityService.create({ - provider: collection.provider, - service: collection.service, - collection: collection.id, - data: entity.toJson(), - options, - uid - }); - - const createdEntity = new EntityObject().fromJson(response); - entities.value.push(createdEntity); - - console.debug('[People Manager](Store) - Successfully created entity'); - - return createdEntity; + console.debug('[People Manager][Store] - Successfully created entity:', key) + return response } catch (error: any) { - console.error('[People Manager](Store) - Failed to create entity:', error); - throw error; - } - } - - /** - * Modify an existing entity - */ - async function modify( - collection: CollectionObject, - entity: EntityObject, - uid?: string - ): Promise { - try { - if (!collection.provider || !collection.service || !collection.id) { - throw new Error('Collection must have provider, service, and id'); - } - if (!entity.in || !entity.id) { - throw new Error('Invalid entity object, must have an collection and entity identifier'); - } - if (collection.id !== entity.in) { - throw new Error('Invalid entity object, does not belong to the specified collection'); - } - - const response = await entityService.modify({ - provider: collection.provider, - service: collection.service, - collection: collection.id, - identifier: entity.id, - data: entity.toJson(), - uid - }); - - const modifiedEntity = new EntityObject().fromJson(response); - const index = entities.value.findIndex(e => e.id === entity.id); - if (index !== -1) { - entities.value[index] = modifiedEntity; - } - - console.debug('[People Manager](Store) - Successfully modified entity'); - - return modifiedEntity; - } catch (error: any) { - console.error('[People Manager](Store) - Failed to modify entity:', error); - throw error; + console.error('[People Manager][Store] - Failed to create entity:', error) + throw error + } finally { + transceiving.value = false } } /** - * Delete an entity + * Update an existing entity with given provider, service, collection, identifier, and data + * + * @param provider - provider identifier for the entity to update + * @param service - service identifier for the entity to update + * @param collection - collection identifier for the entity to update + * @param identifier - entity identifier for the entity to update + * @param data - entity properties for update + * + * @returns Promise with updated entity object */ - async function destroy( - collection: CollectionObject, - entity: EntityObject, - uid?: string - ): Promise { + async function update(provider: string, service: string | number, collection: string | number, identifier: string | number, data: any): Promise { + transceiving.value = true try { - if (!collection.provider || !collection.service || !collection.id) { - throw new Error('Collection must have provider, service, and id'); - } - if (!entity.in || !entity.id) { - throw new Error('Invalid entity object, must have an collection and entity identifier'); - } - if (collection.id !== entity.in) { - throw new Error('Invalid entity object, does not belong to the specified collection'); - } + const response = await entityService.update({ provider, service, collection, identifier, properties: data }) + + // Update entity in state + const key = identifierKey(response.provider, response.service, response.collection, response.identifier) + _entities.value[key] = response - const response = await entityService.destroy({ - provider: collection.provider, - service: collection.service, - collection: collection.id, - identifier: entity.id, - uid - }); - - if (response.success) { - const index = entities.value.findIndex(e => e.id === entity.id); - if (index !== -1) { - entities.value.splice(index, 1); - } - } - - console.debug('[People Manager](Store) - Successfully destroyed entity'); - - return response.success; + console.debug('[People Manager][Store] - Successfully updated entity:', key) + return response } catch (error: any) { - console.error('[People Manager](Store) - Failed to destroy entity:', error); - throw error; + console.error('[People Manager][Store] - Failed to update entity:', error) + throw error + } finally { + transceiving.value = false } } + /** + * Delete an entity by provider, service, collection, and identifier + * + * @param provider - provider identifier for the entity to delete + * @param service - service identifier for the entity to delete + * @param collection - collection identifier for the entity to delete + * @param identifier - entity identifier for the entity to delete + * + * @returns Promise with deletion result + */ + async function remove(provider: string, service: string | number, collection: string | number, identifier: string | number): Promise { + transceiving.value = true + try { + const response = await entityService.delete({ provider, service, collection, identifier }) + + // Remove entity from state + const key = identifierKey(provider, service, collection, identifier) + delete _entities.value[key] + + console.debug('[People Manager][Store] - Successfully deleted entity:', key) + return response + } catch (error: any) { + console.error('[People Manager][Store] - Failed to delete entity:', error) + throw error + } finally { + transceiving.value = false + } + } + + /** + * Retrieve delta changes for entities + * + * @param sources - source selector for delta check + * + * @returns Promise with delta changes (additions, modifications, deletions) + * + * Note: Delta returns only identifiers, not full entities. + * Caller should fetch full entities for additions/modifications separately. + */ + async function delta(sources: SourceSelector) { + transceiving.value = true + try { + const response = await entityService.delta({ sources }) + + // Process delta and update store + Object.entries(response).forEach(([provider, providerData]) => { + // Skip if no changes for provider + if (providerData === false) return + + Object.entries(providerData).forEach(([service, serviceData]) => { + // Skip if no changes for service + if (serviceData === false) return + + Object.entries(serviceData).forEach(([collection, collectionData]) => { + // Skip if no changes for collection + if (collectionData === false) return + + // Process deletions (remove from store) + if (collectionData.deletions && collectionData.deletions.length > 0) { + collectionData.deletions.forEach((identifier) => { + const key = identifierKey(provider, service, collection, identifier) + delete _entities.value[key] + }) + } + + // Note: additions and modifications contain only identifiers + // The caller should fetch full entities using the fetch() method + }) + }) + }) + + console.debug('[People Manager][Store] - Successfully processed delta changes') + return response + } catch (error: any) { + console.error('[People Manager][Store] - Failed to process delta:', error) + throw error + } finally { + transceiving.value = false + } + } + + // Return public API return { - // State + // State (readonly) + transceiving: readonly(transceiving), + // Getters + count, + has, entities, - + entitiesForCollection, // Actions - reset, + entity, list, fetch, - fresh, + extant, create, - modify, - destroy, + update, + delete: remove, + delta, } -}); +}) diff --git a/src/stores/index.ts b/src/stores/index.ts index 4d03b97..800c024 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -1,8 +1,4 @@ -/** - * Central export point for all People Manager stores - */ - -export { useCollectionsStore } from './collectionsStore'; -export { useEntitiesStore } from './entitiesStore'; export { useProvidersStore } from './providersStore'; export { useServicesStore } from './servicesStore'; +export { useCollectionsStore } from './collectionsStore'; +export { useEntitiesStore } from './entitiesStore'; diff --git a/src/stores/providersStore.ts b/src/stores/providersStore.ts index 4fddf0a..f465ee0 100644 --- a/src/stores/providersStore.ts +++ b/src/stores/providersStore.ts @@ -1,62 +1,142 @@ /** - * People Manager - Providers Store + * Providers Store */ -import { defineStore } from 'pinia'; -import { ref } from 'vue'; -import { providerService } from '../services/providerService'; -import type { - SourceSelector, - ProviderInterface, -} from '../types'; +import { ref, computed, readonly } from 'vue' +import { defineStore } from 'pinia' +import { providerService } from '../services' +import { ProviderObject } from '../models/provider' +import type { SourceSelector } from '../types' -export const useProvidersStore = defineStore('providersStore', () => { +export const useProvidersStore = defineStore('peopleProvidersStore', () => { // State - const providers = ref>({}); + const _providers = ref>({}) + const transceiving = ref(false) + + /** + * Get count of providers in store + */ + const count = computed(() => Object.keys(_providers.value).length) + + /** + * Check if any providers are present in store + */ + const has = computed(() => count.value > 0) + + /** + * Get all providers present in store + */ + const providers = computed(() => Object.values(_providers.value)) + + /** + * Get a specific provider from store, with optional retrieval + * + * @param identifier - Provider identifier + * @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only + * + * @returns Provider object or null + */ + function provider(identifier: string, retrieve: boolean = false): ProviderObject | null { + if (retrieve === true && !_providers.value[identifier]) { + console.debug(`[People Manager][Store] - Force fetching provider "${identifier}"`) + fetch(identifier) + } + + return _providers.value[identifier] || null + } // Actions /** - * List all available providers - * - * @returns Promise with provider list keyed by provider ID + * Retrieve all or specific providers, optionally filtered by source selector + * + * @param request - list request parameters + * + * @returns Promise with provider object list keyed by provider identifier */ - async function list(): Promise> { + async function list(sources?: SourceSelector): Promise> { + transceiving.value = true try { - const response = await providerService.list(); - - console.debug('[People Manager](Store) - Successfully retrieved', Object.keys(response).length, 'providers:', Object.keys(response)); - - providers.value = response; - return response; + const providers = await providerService.list({ sources }) + + // Merge retrieved providers into state + _providers.value = { ..._providers.value, ...providers } + + console.debug('[People Manager][Store] - Successfully retrieved', Object.keys(providers).length, 'providers') + return providers } catch (error: any) { - console.error('[People Manager](Store) - Failed to retrieve providers:', error); - throw error; + console.error('[People Manager][Store] - Failed to retrieve providers:', error) + throw error + } finally { + transceiving.value = false } } /** - * Check which providers exist/are available - * - * @param sources - Source selector with provider IDs to check - * @returns Promise with provider availability status + * Retrieve a specific provider by identifier + * + * @param identifier - provider identifier + * + * @returns Promise with provider object */ - async function extant(sources: SourceSelector): Promise> { + async function fetch(identifier: string): Promise { + transceiving.value = true try { - const response = await providerService.extant(sources); - return response; + const provider = await providerService.fetch({ identifier }) + + // Merge fetched provider into state + _providers.value[provider.identifier] = provider + + console.debug('[People Manager][Store] - Successfully fetched provider:', provider.identifier) + return provider } catch (error: any) { - console.error('[People Manager](Store) - Failed to check provider existence:', error); - throw error; + console.error('[People Manager][Store] - Failed to fetch provider:', error) + throw error + } finally { + transceiving.value = false } } + /** + * Retrieve provider availability status for a given source selector + * + * @param sources - source selector to check availability for + * + * @returns Promise with provider availability status + */ + async function extant(sources: SourceSelector) { + transceiving.value = true + try { + const response = await providerService.extant({ sources }) + + Object.entries(response).forEach(([providerId, providerStatus]) => { + if (providerStatus === false) { + delete _providers.value[providerId] + } + }) + + console.debug('[People Manager][Store] - Successfully checked', sources ? Object.keys(sources).length : 0, 'providers') + return response + } catch (error: any) { + console.error('[People Manager][Store] - Failed to check providers:', error) + throw error + } finally { + transceiving.value = false + } + } + + // Return public API return { // State + transceiving: readonly(transceiving), + // computed + count, + has, providers, - - // Actions + provider, + // functions list, + fetch, extant, - }; -}); + } +}) diff --git a/src/stores/servicesStore.ts b/src/stores/servicesStore.ts index 818c8b4..f9e28a3 100644 --- a/src/stores/servicesStore.ts +++ b/src/stores/servicesStore.ts @@ -1,95 +1,259 @@ /** - * People Manager - Services Store + * Services Store */ -import { defineStore } from 'pinia'; -import { ref } from 'vue'; -import { serviceService } from '../services/serviceService'; -import { ServiceObject } from '../models/service'; -import type { ServiceInterface } from '../types/service'; +import { ref, computed, readonly } from 'vue' +import { defineStore } from 'pinia' +import { serviceService } from '../services' +import { ServiceObject } from '../models/service' import type { SourceSelector, - ListFilter, - ListSort, -} from '../types/common'; + ServiceInterface, +} from '../types' export const useServicesStore = defineStore('peopleServicesStore', () => { // State - const services = ref([]); - // Actions + const _services = ref>({}) + const transceiving = ref(false) /** - * Retrieve services from the server + * Get count of services in store */ - async function list( - sources?: SourceSelector, - filter?: ListFilter, - sort?: ListSort, - uid?: string - ): Promise { - try { - const response = await serviceService.list({ sources, filter, sort, uid }); + const count = computed(() => Object.keys(_services.value).length) - // Flatten the nested response into a flat array - const flatServices: ServiceObject[] = []; - Object.entries(response).forEach(([providerId, providerServices]) => { - Object.values(providerServices).forEach((service: ServiceInterface) => { - // Ensure provider is set on the service object - service.provider = service.provider || providerId; - flatServices.push(new ServiceObject().fromJson(service)); - }); - }); + /** + * Check if any services are present in store + */ + const has = computed(() => count.value > 0) - console.debug('[People Manager](Store) - Successfully retrieved', flatServices.length, 'services:', flatServices.map(s => ({ - id: s.id, - label: s.label, - provider: s.provider - }))); + /** + * Get all services present in store + */ + const services = computed(() => Object.values(_services.value)) - services.value = flatServices; - return flatServices; - } catch (error: any) { - console.error('[People Manager](Store) - Failed to retrieve services:', error); - throw error; + /** + * Get all services present in store grouped by provider + */ + const servicesByProvider = computed(() => { + const groups: Record = {} + + Object.values(_services.value).forEach((service) => { + const providerServices = (groups[service.provider] ??= []) + providerServices.push(service) + }) + + return groups + }) + + /** + * Get a specific service from store, with optional retrieval + * + * @param provider - provider identifier + * @param identifier - service identifier + * @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only + * + * @returns Service object or null + */ + function service(provider: string, identifier: string | number, retrieve: boolean = false): ServiceObject | null { + const key = identifierKey(provider, identifier) + if (retrieve === true && !_services.value[key]) { + console.debug(`[People Manager][Store] - Force fetching service "${key}"`) + fetch(provider, identifier) } + + return _services.value[key] || null } /** - * Fetch a specific service - * - * @param provider - Provider identifier - * @param identifier - Service identifier - * @param uid - Optional user identifier + * Unique key for a service + */ + function identifierKey(provider: string, identifier: string | number | null): string { + return `${provider}:${identifier ?? ''}` + } + + // Actions + + /** + * Retrieve all or specific services, optionally filtered by source selector + * + * @param sources - optional source selector + * + * @returns Promise with service object list keyed by provider and service identifier + */ + async function list(sources?: SourceSelector): Promise> { + transceiving.value = true + try { + const response = await serviceService.list({ sources }) + + // Flatten nested structure: provider-id: { service-id: object } -> "provider-id:service-id": object + const services: Record = {} + Object.entries(response).forEach(([_providerId, providerServices]) => { + Object.entries(providerServices).forEach(([_serviceId, serviceObj]) => { + const key = identifierKey(serviceObj.provider, serviceObj.identifier) + services[key] = serviceObj + }) + }) + + // Merge retrieved services into state + _services.value = { ..._services.value, ...services } + + console.debug('[People Manager][Store] - Successfully retrieved', Object.keys(services).length, 'services') + return services + } catch (error: any) { + console.error('[People Manager][Store] - Failed to retrieve services:', error) + throw error + } finally { + transceiving.value = false + } + } + + /** + * Retrieve a specific service by provider and identifier + * + * @param provider - provider identifier + * @param identifier - service identifier + * * @returns Promise with service object */ - async function fetch( - provider: string, - identifier: string, - uid?: string - ): Promise { + async function fetch(provider: string, identifier: string | number): Promise { + transceiving.value = true try { - const response = await serviceService.fetch({ provider, service: identifier, uid }); - return new ServiceObject().fromJson(response); + const service = await serviceService.fetch({ provider, identifier }) + + // Merge fetched service into state + const key = identifierKey(service.provider, service.identifier) + _services.value[key] = service + + console.debug('[People Manager][Store] - Successfully fetched service:', key) + return service } catch (error: any) { - console.error('[People Manager](Store) - Failed to fetch service:', error); - throw error; + console.error('[People Manager][Store] - Failed to fetch service:', error) + throw error + } finally { + transceiving.value = false } } /** - * Create a fresh service object with default values + * Retrieve service availability status for a given source selector + * + * @param sources - source selector to check availability for + * + * @returns Promise with service availability status */ - function fresh(): ServiceObject { - return new ServiceObject(); + async function extant(sources: SourceSelector) { + transceiving.value = true + try { + const response = await serviceService.extant({ sources }) + + console.debug('[People Manager][Store] - Successfully checked', sources ? Object.keys(sources).length : 0, 'services') + return response + } catch (error: any) { + console.error('[People Manager][Store] - Failed to check services:', error) + throw error + } finally { + transceiving.value = false + } } - return { - // State - services, + /** + * Create a new service with given provider and data + * + * @param provider - provider identifier for the new service + * @param data - partial service data for creation + * + * @returns Promise with created service object + */ + async function create(provider: string, data: Partial): Promise { + transceiving.value = true + try { + const service = await serviceService.create({ provider, data }) + + // Merge created service into state + const key = identifierKey(service.provider, service.identifier) + _services.value[key] = service + + console.debug('[People Manager][Store] - Successfully created service:', key) + return service + } catch (error: any) { + console.error('[People Manager][Store] - Failed to create service:', error) + throw error + } finally { + transceiving.value = false + } + } + /** + * Update an existing service with given provider, identifier, and data + * + * @param provider - provider identifier for the service to update + * @param identifier - service identifier for the service to update + * @param data - partial service data for update + * + * @returns Promise with updated service object + */ + async function update(provider: string, identifier: string | number, data: Partial): Promise { + transceiving.value = true + try { + const service = await serviceService.update({ provider, identifier, data }) + + // Merge updated service into state + const key = identifierKey(service.provider, service.identifier) + _services.value[key] = service + + console.debug('[People Manager][Store] - Successfully updated service:', key) + return service + } catch (error: any) { + console.error('[People Manager][Store] - Failed to update service:', error) + throw error + } finally { + transceiving.value = false + } + } + + /** + * Delete a service by provider and identifier + * + * @param provider - provider identifier for the service to delete + * @param identifier - service identifier for the service to delete + * + * @returns Promise with deletion result + */ + async function remove(provider: string, identifier: string | number): Promise { + transceiving.value = true + try { + await serviceService.delete({ provider, identifier }) + + // Remove deleted service from state + const key = identifierKey(provider, identifier) + delete _services.value[key] + + console.debug('[People Manager][Store] - Successfully deleted service:', key) + } catch (error: any) { + console.error('[People Manager][Store] - Failed to delete service:', error) + throw error + } finally { + transceiving.value = false + } + } + + // Return public API + return { + // State (readonly) + transceiving: readonly(transceiving), + // Getters + count, + has, + services, + servicesByProvider, + // Actions + service, list, fetch, - fresh, - }; -}); + extant, + create, + update, + delete: remove, + } +}) diff --git a/src/types/collection.ts b/src/types/collection.ts index 2c16000..856f61e 100644 --- a/src/types/collection.ts +++ b/src/types/collection.ts @@ -1,68 +1,45 @@ /** - * Collection-related type definitions for People Manager + * Collection type definitions */ - -import type { ListFilter, ListSort, SourceSelector } from "./common"; +import type { ListFilter, ListSort, SourceSelector } from './common'; /** - * Permission settings for a collection - */ -export interface CollectionPermissionInterface { - view: boolean; - create: boolean; - modify: boolean; - destroy: boolean; - share: boolean; -} - -/** - * Permissions settings for multiple users in a collection - */ -export interface CollectionPermissionsInterface { - [userId: string]: CollectionPermissionInterface; -} - -/** - * Role settings for a collection - */ -export interface CollectionRolesInterface { - individual?: boolean; - [roleType: string]: boolean | undefined; -} - -/** - * Content type settings for a collection - */ -export interface CollectionContentsInterface { - individual?: boolean; - organization?: boolean; - group?: boolean; - [contentType: string]: boolean | undefined; -} - -/** - * Represents a collection within a service + * Collection information */ export interface CollectionInterface { - '@type': string; - provider: string | null; - service: string | null; - in: number | string | null; - id: number | string | null; - label: string | null; - description: string | null; - priority: number | null; - visibility: string | null; - color: string | null; - enabled: boolean; - signature: string | null; - permissions: CollectionPermissionsInterface; - roles: CollectionRolesInterface; - contents: CollectionContentsInterface; + provider: string; + service: string | number; + collection: string | number | null; + identifier: string | number; + signature?: string | null; + created?: string | null; + modified?: string | null; + properties: CollectionPropertiesInterface; } +export type CollectionContentTypes = 'individual' | 'organization' | 'group'; + +export interface CollectionBaseProperties { + '@type': string; + version: number; +} + +export interface CollectionImmutableProperties extends CollectionBaseProperties { + content: CollectionContentTypes[]; +} + +export interface CollectionMutableProperties extends CollectionBaseProperties { + label: string; + description: string | null; + rank: number | null; + visibility: boolean | null; + color: string | null; +} + +export interface CollectionPropertiesInterface extends CollectionMutableProperties, CollectionImmutableProperties {} + /** - * Request to collection list endpoint + * Collection list */ export interface CollectionListRequest { sources?: SourceSelector; @@ -70,9 +47,6 @@ export interface CollectionListRequest { sort?: ListSort; } -/** - * Response from collection list endpoint - */ export interface CollectionListResponse { [providerId: string]: { [serviceId: string]: { @@ -82,15 +56,23 @@ export interface CollectionListResponse { } /** - * Request to collection extant endpoint + * Collection fetch + */ +export interface CollectionFetchRequest { + provider: string; + service: string | number; + collection: string | number; +} + +export interface CollectionFetchResponse extends CollectionInterface {} + +/** + * Collection extant */ export interface CollectionExtantRequest { sources: SourceSelector; } -/** - * Response from collection extant endpoint - */ export interface CollectionExtantResponse { [providerId: string]: { [serviceId: string]: { @@ -100,62 +82,41 @@ export interface CollectionExtantResponse { } /** - * Request to collection fetch endpoint - */ -export interface CollectionFetchRequest { - provider: string; - service: string; - identifier: string | number; -} - -/** - * Response from collection fetch endpoint - */ -export interface CollectionFetchResponse extends CollectionInterface {} - -/** - * Request to collection create endpoint + * Collection create */ export interface CollectionCreateRequest { provider: string; - service: string; - data: CollectionInterface; - options?: (string)[]; + service: string | number; + collection?: string | number | null; // Parent Collection Identifier + properties: CollectionMutableProperties; } -/** - * Response from collection create endpoint - */ -export interface CollectionCreateResponse extends CollectionInterface {} +export interface CollectionCreateResponse extends CollectionInterface {} /** - * Request to collection modify endpoint + * Collection modify */ -export interface CollectionModifyRequest { +export interface CollectionUpdateRequest { provider: string; - service: string; + service: string | number; identifier: string | number; - data: CollectionInterface; + properties: CollectionMutableProperties; } -/** - * Response from collection modify endpoint - */ -export interface CollectionModifyResponse extends CollectionInterface {} +export interface CollectionUpdateResponse extends CollectionInterface {} /** - * Request to collection destroy endpoint + * Collection delete */ -export interface CollectionDestroyRequest { +export interface CollectionDeleteRequest { provider: string; - service: string; + service: string | number; identifier: string | number; + options?: { + force?: boolean; // Whether to force delete even if collection is not empty + }; } - -/** - * Response from collection destroy endpoint - */ -export interface CollectionDestroyResponse { +export interface CollectionDeleteResponse { success: boolean; } diff --git a/src/types/common.ts b/src/types/common.ts index 8b667a8..ab912d4 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -1,9 +1,7 @@ /** - * Common types shared across People Manager services + * Common types shared across provider, service, collection, and entity request and responses. */ -import type { FilterComparisonOperator, FilterConjunctionOperator } from './service'; - /** * Base API request envelope */ @@ -46,14 +44,19 @@ export interface ApiErrorResponse { export type ApiResponse = ApiSuccessResponse | ApiErrorResponse; /** - * Source selector structure for hierarchical resource selection - * Structure: Provider -> Service -> Collection -> Entity + * Selector for targeting specific providers, services, collections, or entities in list or extant operations. * - * Examples: - * - Simple boolean: { "local": true } - * - Nested services: { "system": { "personal": true, "recents": true } } - * - Collection IDs: { "system": { "personal": { "299": true, "176": true } } } - * - Entity IDs: { "system": { "personal": { "299": [1350, 1353, 5000] } } } + * Example usage: + * { + * "provider1": true, // Select all services/collections/entities under provider1 + * "provider2": { + * "serviceA": true, // Select all collections/entities under serviceA of provider2 + * "serviceB": { + * "collectionX": true, // Select all entities under collectionX of serviceB of provider2 + * "collectionY": [1, 2, 3] // Select entities with identifiers 1, 2, and 3 under collectionY of serviceB of provider2 + * } + * } + * } */ export type SourceSelector = { [provider: string]: boolean | ServiceSelector; @@ -69,39 +72,85 @@ export type CollectionSelector = { export type EntitySelector = (string | number)[]; -/** - * Filter condition for building complex queries - */ -export interface FilterCondition { - attribute: string; - value: string | number | boolean | any[]; - comparator?: FilterComparisonOperator; - conjunction?: FilterConjunctionOperator; -} /** - * Filter criteria for list operations - * Can be simple key-value pairs or complex filter conditions + * Filter comparison for list operations + */ +export const ListFilterComparisonOperator = { + EQ: 1, // Equal + NEQ: 2, // Not Equal + GT: 4, // Greater Than + LT: 8, // Less Than + GTE: 16, // Greater Than or Equal + LTE: 32, // Less Than or Equal + IN: 64, // In Array + NIN: 128, // Not In Array + LIKE: 256, // Like + NLIKE: 512, // Not Like +} as const; + +export type ListFilterComparisonOperator = typeof ListFilterComparisonOperator[keyof typeof ListFilterComparisonOperator]; + +/** + * Filter conjunction for list operations + */ +export const ListFilterConjunctionOperator = { + NONE: '', + AND: 'AND', + OR: 'OR', +} as const; + +export type ListFilterConjunctionOperator = typeof ListFilterConjunctionOperator[keyof typeof ListFilterConjunctionOperator]; + +/** + * Filter condition for list operations + * + * Tuple format: [value, comparator?, conjunction?] + */ +export type ListFilterCondition = [ + string | number | boolean | string[] | number[], + ListFilterComparisonOperator?, + ListFilterConjunctionOperator? +]; + +/** + * Filter for list operations + * + * Values can be: + * - Simple primitives (string | number | boolean) for default equality comparison + * - ListFilterCondition tuple for explicit comparator/conjunction + * + * Examples: + * - Simple usage: { name: "John" } + * - With comparator: { age: [25, ListFilterComparisonOperator.GT] } + * - With conjunction: { age: [25, ListFilterComparisonOperator.GT, ListFilterConjunctionOperator.AND] } + * - With array value for IN operator: { status: [["active", "pending"], ListFilterComparisonOperator.IN] } */ export interface ListFilter { - label?: string; - [key: string]: any; + [attribute: string]: string | number | boolean | ListFilterCondition; } /** - * Sort options for list operations + * Sort for list operations + * + * Values can be: + * - true for ascending + * - false for descending */ export interface ListSort { - [key: string]: boolean; + [attribute: string]: boolean; } /** - * Range specification for pagination/limiting results + * Range for list operations + * + * Values can be: + * - relative based on item identifier + * - absolute based on item count */ export interface ListRange { type: 'tally'; - anchor: 'absolute' | 'relative'; - position: number; + anchor: 'relative' | 'absolute'; + position: string | number; tally: number; -} - +} \ No newline at end of file diff --git a/src/types/entity.ts b/src/types/entity.ts index 248995e..8c4295c 100644 --- a/src/types/entity.ts +++ b/src/types/entity.ts @@ -1,30 +1,27 @@ +/** + * Entity type definitions + */ import type { ListFilter, ListRange, ListSort, SourceSelector } from './common'; +import type { GroupInterface } from './group'; import type { IndividualInterface } from './individual'; import type { OrganizationInterface } from './organization'; -import type { GroupInterface } from './group'; /** - * Entity-related type definitions for People Manager + * Entity definition */ - -/** - * Represents a person entity (contact) - */ -export interface EntityInterface { - '@type': string; - version: number; - in: string | number | null; - id: string | number | null; - createdOn: Date | null; - createdBy: string | null; - modifiedOn: Date | null; - modifiedBy: string | null; +export interface EntityInterface { + provider: string; + service: string; + collection: string | number; + identifier: string | number; signature: string | null; - data: IndividualInterface | OrganizationInterface | GroupInterface | null; + created: string | null; + modified: string | null; + properties: T; } /** - * Request to entity list endpoint + * Entity list */ export interface EntityListRequest { sources?: SourceSelector; @@ -33,127 +30,102 @@ export interface EntityListRequest { range?: ListRange; } -/** - * Response from entity list endpoint - */ export interface EntityListResponse { [providerId: string]: { [serviceId: string]: { [collectionId: string]: { - [entityId: string]: EntityInterface; + [identifier: string]: EntityInterface; }; }; }; } /** - * Request to entity delta endpoint + * Entity fetch */ -export interface EntityDeltaRequest { - sources: SourceSelector; +export interface EntityFetchRequest { + provider: string; + service: string | number; + collection: string | number; + identifiers: (string | number)[]; +} + +export interface EntityFetchResponse { + [identifier: string]: EntityInterface; } /** - * Response from entity delta endpoint - */ -export interface EntityDeltaResponse { - [providerId: string]: { - [serviceId: string]: { - [collectionId: string]: { - signature: string; - created?: { - [entityId: string]: EntityInterface; - }; - modified?: { - [entityId: string]: EntityInterface; - }; - deleted?: string[]; // Array of deleted entity IDs - }; - }; - }; -} - -/** - * Request to entity extant endpoint + * Entity extant */ export interface EntityExtantRequest { sources: SourceSelector; } -/** - * Response from entity extant endpoint - */ export interface EntityExtantResponse { [providerId: string]: { [serviceId: string]: { [collectionId: string]: { - [entityId: string]: boolean; + [identifier: string]: boolean; }; }; }; } /** - * Request to entity fetch endpoint + * Entity create */ -export interface EntityFetchRequest { +export interface EntityCreateRequest { provider: string; - service: string; + service: string | number; collection: string | number; - identifiers: (string | number)[]; + properties: T; } -/** - * Response from entity fetch endpoint - */ -export interface EntityFetchResponse extends Record {} +export interface EntityCreateResponse extends EntityInterface {} /** - * Request to entity create endpoint + * Entity update */ -export interface EntityCreateRequest { +export interface EntityUpdateRequest { provider: string; - service: string; - collection: string | number; - data: EntityInterface; - options?: (string)[]; -} - -/** - * Response from entity create endpoint - */ -export interface EntityCreateResponse extends EntityInterface {} - -/** - * Request to entity modify endpoint - */ -export interface EntityModifyRequest { - provider: string; - service: string; + service: string | number; collection: string | number; identifier: string | number; - data: EntityInterface; - options?: (string)[]; + properties: T; } -/** - * Response from entity modify endpoint - */ -export interface EntityModifyResponse extends EntityInterface {} +export interface EntityUpdateResponse extends EntityInterface {} /** - * Request to entity destroy endpoint + * Entity delete */ -export interface EntityDestroyRequest { +export interface EntityDeleteRequest { provider: string; - service: string; + service: string | number; collection: string | number; identifier: string | number; } -/** - * Response from entity destroy endpoint - */ -export interface EntityDestroyResponse { +export interface EntityDeleteResponse { success: boolean; } + +/** + * Entity delta + */ +export interface EntityDeltaRequest { + sources: SourceSelector; +} + +export interface EntityDeltaResponse { + [providerId: string]: false | { + [serviceId: string]: false | { + [collectionId: string]: false | { + signature: string; + additions: (string | number)[]; + modifications: (string | number)[]; + deletions: (string | number)[]; + }; + }; + }; +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 34f147f..b73ea6c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,7 +1,3 @@ -/** - * Central export point for all People Manager types - */ - export type * from './collection'; export type * from './common'; export type * from './entity'; diff --git a/src/types/provider.ts b/src/types/provider.ts index 2db5be0..b9632c8 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -1,5 +1,5 @@ /** - * Provider-specific types + * Provider type definitions */ import type { SourceSelector } from "./common"; @@ -11,9 +11,11 @@ export interface ProviderCapabilitiesInterface { ServiceFetch?: boolean; ServiceExtant?: boolean; ServiceCreate?: boolean; - ServiceModify?: boolean; + ServiceUpdate?: boolean; ServiceDelete?: boolean; - [key: string]: boolean | undefined; + ServiceDiscover?: boolean; + ServiceTest?: boolean; + [key: string]: boolean | object | string[] | undefined; } /** @@ -21,33 +23,38 @@ export interface ProviderCapabilitiesInterface { */ export interface ProviderInterface { '@type': string; - id: string; + identifier: string; label: string; capabilities: ProviderCapabilitiesInterface; } /** - * Request to provider list endpoint + * Provider list */ -export interface ProviderListRequest {} +export interface ProviderListRequest { + sources?: SourceSelector; +} -/** - * Response from provider list endpoint - */ export interface ProviderListResponse { - [providerId: string]: ProviderInterface; + [identifier: string]: ProviderInterface; } /** - * Request to provider extant endpoint + * Provider fetch + */ +export interface ProviderFetchRequest { + identifier: string; +} + +export interface ProviderFetchResponse extends ProviderInterface {} + +/** + * Provider extant */ export interface ProviderExtantRequest { sources: SourceSelector; } -/** - * Response from provider extant endpoint - */ export interface ProviderExtantResponse { - [providerId: string]: boolean; + [identifier: string]: boolean; } diff --git a/src/types/service.ts b/src/types/service.ts index 6932a6b..f583141 100644 --- a/src/types/service.ts +++ b/src/types/service.ts @@ -1,40 +1,252 @@ /** - * Service-related type definitions for People Manager + * Service type definitions */ - -import type { ListFilter, ListSort, SourceSelector } from "./common"; +import type { SourceSelector, ListFilterComparisonOperator } from './common'; /** - * Filter comparison operators (bitmask values) + * Service capabilities */ -export const FilterComparisonOperator = { - EQ: 1, // Equal - NEQ: 2, // Not Equal - GT: 4, // Greater Than - LT: 8, // Less Than - GTE: 16, // Greater Than or Equal - LTE: 32, // Less Than or Equal - IN: 64, // In Array - NIN: 128, // Not In Array - LIKE: 256, // Like (pattern matching) - NLIKE: 512, // Not Like -} as const; - -export type FilterComparisonOperator = typeof FilterComparisonOperator[keyof typeof FilterComparisonOperator]; +export interface ServiceCapabilitiesInterface { + // Collection capabilities + CollectionList?: boolean; + CollectionListFilter?: ServiceListFilterCollection; + CollectionListSort?: ServiceListSortCollection; + CollectionExtant?: boolean; + CollectionFetch?: boolean; + CollectionCreate?: boolean; + CollectionUpdate?: boolean; + CollectionDelete?: boolean; + // Message capabilities + EntityList?: boolean; + EntityListFilter?: ServiceListFilterEntity; + EntityListSort?: ServiceListSortEntity; + EntityListRange?: ServiceListRange; + EntityDelta?: boolean; + EntityExtant?: boolean; + EntityFetch?: boolean; + EntityCreate?: boolean; + EntityUpdate?: boolean; + EntityDelete?: boolean; + EntityMove?: boolean; + EntityCopy?: boolean; + [key: string]: boolean | object | string | string[] | undefined; +} /** - * Filter conjunction operators + * Service information */ -export const FilterConjunctionOperator = { - NONE: '', - AND: 'AND', - OR: 'OR', -} as const; - -export type FilterConjunctionOperator = typeof FilterConjunctionOperator[keyof typeof FilterConjunctionOperator]; +export interface ServiceInterface { + '@type': string; + provider: string; + identifier: string | number | null; + label: string | null; + enabled: boolean; + capabilities?: ServiceCapabilitiesInterface; + location?: ServiceLocation | null; + identity?: ServiceIdentity | null; + auxiliary?: Record; // Provider-specific extension data +} /** - * Filter specification format + * Service list + */ +export interface ServiceListRequest { + sources?: SourceSelector; +} + +export interface ServiceListResponse { + [provider: string]: { + [identifier: string]: ServiceInterface; + }; +} + +/** + * Service fetch + */ +export interface ServiceFetchRequest { + provider: string; + identifier: string | number; +} + +export interface ServiceFetchResponse extends ServiceInterface {} + +/** + * Service extant + */ +export interface ServiceExtantRequest { + sources: SourceSelector; +} + +export interface ServiceExtantResponse { + [provider: string]: { + [identifier: string]: boolean; + }; +} + +/** + * Service create + */ +export interface ServiceCreateRequest { + provider: string; + data: Partial; +} + +export interface ServiceCreateResponse extends ServiceInterface {} + +/** + * Service update + */ +export interface ServiceUpdateRequest { + provider: string; + identifier: string | number; + data: Partial; +} + +export interface ServiceUpdateResponse extends ServiceInterface {} + +/** + * Service delete + */ +export interface ServiceDeleteRequest { + provider: string; + identifier: string | number; +} + +export interface ServiceDeleteResponse {} + +/** + * Service discovery + */ +export interface ServiceDiscoverRequest { + identity: string; // Email address or domain + provider?: string; // Optional: specific provider ('jmap', 'smtp', etc.) or null for all + location?: string; // Optional: known hostname (bypasses DNS lookup) + secret?: string; // Optional: password/token for credential validation +} + +export interface ServiceDiscoverResponse { + [provider: string]: ServiceLocation; // Uses existing ServiceLocation discriminated union +} + +/** + * Service connection test + */ +export interface ServiceTestRequest { + provider: string; + // For existing service + identifier?: string | number | null; + // For fresh configuration + location?: ServiceLocation | null; + identity?: ServiceIdentity | null; +} + +export interface ServiceTestResponse { + success: boolean; + message: string; +} + +/** + * Service location - Base + */ +export interface ServiceLocationBase { + type: 'URI' | 'FILE'; +} + +/** + * Service location - URI-based type + */ +export interface ServiceLocationUri extends ServiceLocationBase { + type: 'URI'; + scheme: string; // e.g., 'https', 'http' + host: string; // e.g., 'api.example.com' + port: number; // e.g., 443 + path?: string; // e.g., '/v1/api' + verifyPeer?: boolean; // Verify SSL/TLS peer certificate + verifyHost?: boolean; // Verify SSL/TLS certificate host +} + +/** + * Service location - File-based type (e.g., for local mail delivery or Unix socket) + */ +export interface ServiceLocationFile extends ServiceLocationBase { + type: 'FILE'; + path: string; // File system path +} + +/** + * Service location types + */ +export type ServiceLocation = + | ServiceLocationUri + | ServiceLocationFile; + +/** + * Service identity - base + */ +export interface ServiceIdentityBase { + type: 'NA' | 'BA' | 'TA' | 'OA' | 'CC'; +} + +/** + * Service identity - No authentication + */ +export interface ServiceIdentityNone extends ServiceIdentityBase { + type: 'NA'; +} + +/** + * Service identity - Basic authentication type + */ +export interface ServiceIdentityBasic extends ServiceIdentityBase { + type: 'BA'; + identity: string; // Username/email + secret: string; // Password +} + +/** + * Token authentication (API key, static token) + */ +export interface ServiceIdentityToken extends ServiceIdentityBase { + type: 'TA'; + token: string; // Authentication token/API key +} + +/** + * OAuth authentication + */ +export interface ServiceIdentityOAuth extends ServiceIdentityBase { + type: 'OA'; + accessToken: string; // Current access token + accessScope?: string[]; // Token scopes + accessExpiry?: number; // Unix timestamp when token expires + refreshToken?: string; // Refresh token for getting new access tokens + refreshLocation?: string; // Token refresh endpoint URL +} + +/** + * Client certificate authentication (mTLS) + */ +export interface ServiceIdentityCertificate extends ServiceIdentityBase { + type: 'CC'; + certificate: string; // X.509 certificate (PEM format or file path) + privateKey: string; // Private key (PEM format or file path) + passphrase?: string; // Optional passphrase for encrypted private key +} + +/** + * Service identity configuration + * Discriminated union of all identity types + */ +export type ServiceIdentity = + | ServiceIdentityNone + | ServiceIdentityBasic + | ServiceIdentityToken + | ServiceIdentityOAuth + | ServiceIdentityCertificate; + +/** + * List filter specification format + * * Format: "type:length:defaultComparator:supportedComparators" * * Examples: @@ -50,162 +262,39 @@ export type FilterConjunctionOperator = typeof FilterConjunctionOperator[keyof t * * Comparator values are bitmasks that can be combined */ -export type FilterSpec = string; +export type ServiceListFilterCollection = { + 'label'?: string; + 'rank'?: string; + [attribute: string]: string | undefined; +}; + +export type ServiceListFilterEntity = { + 'text'?: string; + 'label'?: string; + 'name'?: string; + 'name.given'?: string; + 'name.family'?: string; + 'organization'?: string; + 'email'?: string; + 'phone'?: string; + 'address'?: string; + [attribute: string]: string | undefined; +} /** - * Parsed filter specification + * Service list sort specification */ -export interface ParsedFilterSpec { - type: 'string' | 'integer' | 'boolean' | 'array'; +export type ServiceListSortCollection = ("label" | "rank" | string)[]; +export type ServiceListSortEntity = ( "label" | "name" | "name.given" | "name.family" | string)[]; + +export type ServiceListRange = { + 'tally'?: string[]; +}; + + +export interface ServiceListFilterDefinition { + type: 'string' | 'integer' | 'date' | 'boolean' | 'array'; length: number; - defaultComparator: FilterComparisonOperator; - supportedComparators: FilterComparisonOperator[]; + defaultComparator: ListFilterComparisonOperator; + supportedComparators: ListFilterComparisonOperator[]; } - -/** - * Parse a filter specification string into its components - * - * @param spec - Filter specification string (e.g., "s:200:256:771") - * @returns Parsed filter specification object - * - * @example - * parseFilterSpec("s:200:256:771") - * // Returns: { - * // type: 'string', - * // length: 200, - * // defaultComparator: 256 (LIKE), - * // supportedComparators: [1, 2, 256, 512] (EQ, NEQ, LIKE, NLIKE) - * // } - */ -export function parseFilterSpec(spec: FilterSpec): ParsedFilterSpec { - const [typeCode, lengthStr, defaultComparatorStr, supportedComparatorsStr] = spec.split(':'); - - const typeMap: Record = { - 's': 'string', - 'i': 'integer', - 'b': 'boolean', - 'a': 'array', - }; - - const type = typeMap[typeCode]; - if (!type) { - throw new Error(`Invalid filter type code: ${typeCode}`); - } - - const length = parseInt(lengthStr, 10); - const defaultComparator = parseInt(defaultComparatorStr, 10) as FilterComparisonOperator; - - // Parse supported comparators from bitmask - const supportedComparators: FilterComparisonOperator[] = []; - const supportedBitmask = parseInt(supportedComparatorsStr, 10); - - if (supportedBitmask !== 0) { - const allComparators = Object.values(FilterComparisonOperator).filter(v => typeof v === 'number') as number[]; - for (const comparator of allComparators) { - if ((supportedBitmask & comparator) === comparator) { - supportedComparators.push(comparator as FilterComparisonOperator); - } - } - } - - return { - type, - length, - defaultComparator, - supportedComparators, - }; -} - -/** - * Capabilities available for a service - */ -export interface ServiceCapabilitiesInterface { - // Collection capabilities - CollectionList?: boolean; - CollectionListFilter?: { - [key: string]: FilterSpec; - }; - CollectionListSort?: string[]; - CollectionExtant?: boolean; - CollectionFetch?: boolean; - CollectionCreate?: boolean; - CollectionModify?: boolean; - CollectionDestroy?: boolean; - - // Entity capabilities - EntityList?: boolean; - EntityListFilter?: { - [key: string]: FilterSpec; - }; - EntityListSort?: string[]; - EntityListRange?: { - [rangeType: string]: string[]; // e.g., { "tally": ["absolute", "relative"] } - }; - EntityDelta?: boolean; - EntityExtant?: boolean; - EntityFetch?: boolean; - EntityCreate?: boolean; - EntityModify?: boolean; - EntityDestroy?: boolean; - EntityCopy?: boolean; - EntityMove?: boolean; -} - -/** - * Represents a service within a provider - */ -export interface ServiceInterface { - '@type': string; - provider: string; - id: string; - label: string; - capabilities?: ServiceCapabilitiesInterface; - enabled: boolean; -} - -/** - * Request to service list endpoint - */ -export interface ServiceListRequest { - sources?: SourceSelector; - filter?: ListFilter; - sort?: ListSort; -} - -/** - * Response from service list endpoint - */ -export interface ServiceListResponse { - [providerId: string]: { - [serviceId: string]: ServiceInterface; - }; -} - -/** - * Request to service extant endpoint - */ -export interface ServiceExtantRequest { - sources: SourceSelector; -} - -/** - * Response from service extant endpoint - */ -export interface ServiceExtantResponse { - [providerId: string]: { - [serviceId: string]: boolean; - }; -} - -/** - * Request to service fetch endpoint - */ -export interface ServiceFetchRequest { - provider: string; - service: string; -} - -/** - * Response from service fetch endpoint - */ -export interface ServiceFetchResponse extends ServiceInterface {}