UNPKG

@sprucelabs/spruce-cli

Version:

Command line interface for building Spruce skills.

343 lines (288 loc) • 10.5 kB
import pathUtil from 'path' import globby from '@sprucelabs/globby' import { EventContract, EventSignature, PermissionContract, SpruceSchemas, validateEventContract, } from '@sprucelabs/mercury-types' import { Schema, validateSchema } from '@sprucelabs/schema' import { eventResponseUtil, eventDiskUtil, eventNameUtil, buildEmitTargetAndPayloadSchema, eventContractUtil, } from '@sprucelabs/spruce-event-utils' import { diskUtil, namesUtil } from '@sprucelabs/spruce-skill-utils' import { cloneDeep } from 'lodash' import SpruceError from '../../../errors/SpruceError' import AbstractStore from '../../../stores/AbstractStore' import { InternalUpdateHandler } from '../../../types/cli.types' import { eventContractCleanerUtil } from '../../../utilities/eventContractCleaner.utility' export default class EventStore extends AbstractStore { public name = 'event' protected static contractCache: Record<string, any> = {} private static localEventCache?: SpruceSchemas.Mercury.v2020_12_25.EventContract public static clearCache() { EventStore.localEventCache = undefined EventStore.contractCache = {} } public async fetchEventContracts( options?: FetchContractsOptions ): Promise<EventStoreFetchEventContractsResponse> { const { localNamespace, didUpdateHandler, namespaces, shouldOnlySyncRemoteEvents, } = options ?? {} didUpdateHandler?.('Pulling remote contracts...') let contracts try { contracts = await this.fetchRemoteContracts(namespaces) } catch (err: any) { const error = err.options?.responseErrors?.[0] ?? err throw error } if (shouldOnlySyncRemoteEvents) { return { contracts, errors: [], } } const localContract = localNamespace && (await this.loadLocalContract(localNamespace, didUpdateHandler)) if (localNamespace) { this.filterOutLocalEventsFromRemoteContractsMutating( contracts, localNamespace ) } if (localContract) { contracts.push(localContract) } return { contracts, errors: [], } } private async fetchRemoteContracts(namespaces?: string[]) { const key = namespaces?.join('|') ?? '_' if (!EventStore.contractCache[key]) { const client = await this.connectToApi({ shouldAuthAsCurrentSkill: true, }) const [{ contracts }] = await client.emitAndFlattenResponses( 'get-event-contracts::v2020_12_25', { target: { namespaces, }, } ) EventStore.contractCache[key] = contracts } return cloneDeep(EventStore.contractCache[key]) } private filterOutLocalEventsFromRemoteContractsMutating( remoteContracts: EventContract[], localNamespace: string ) { const ns = namesUtil.toKebab(localNamespace) for (const contract of remoteContracts) { const sigs = eventContractUtil.getNamedEventSignatures(contract) for (const sig of sigs) { if (sig.eventNamespace === ns) { delete contract.eventSignatures[sig.fullyQualifiedEventName] } } } } public async loadLocalContract( localNamespace: string, didUpdateHandler?: InternalUpdateHandler ): Promise<EventContract | null> { if (EventStore.localEventCache) { return EventStore.localEventCache } const localMatches = await globby( this.generateGlobbyForLocalEvents() as any ) const ns = namesUtil.toKebab(localNamespace) const eventSignatures: Record<string, EventSignature> = {} const filesByFqenAndEventKey: { fqen: string isSchema: boolean match: string eventKey: string }[] = [] didUpdateHandler?.( `Importing ${localMatches.length} local event signature files...` ) await Promise.all( localMatches.map(async (match) => { let fqen: string | undefined let eventKey: keyof EventImport | undefined try { const { eventName, version } = eventDiskUtil.splitPathToEvent(match) fqen = eventNameUtil.join({ eventName, version, eventNamespace: ns, }) const filename = pathUtil.basename( match ) as keyof typeof eventFileNamesImportKeyMap const map = eventFileNamesImportKeyMap[filename] if (map) { //@ts-ignore eventKey = map.key filesByFqenAndEventKey.push({ fqen, isSchema: map.isSchema, match, eventKey: eventKey as string, }) } } catch (err: any) { throw new SpruceError({ code: 'INVALID_EVENT_CONTRACT', fullyQualifiedEventName: fqen ?? 'Bad event name', brokenProperty: eventKey ?? '*** major failure ***', originalError: err, }) } }) ) const matches = filesByFqenAndEventKey.map((o) => o.match) const importsInOrder = (await this.Service('import').bulkImport( matches )) as Record<string, any>[] const importsByName: Record<string, EventImport> = {} for (let idx = 0; idx < filesByFqenAndEventKey.length; idx++) { const imported = importsInOrder[idx] const { fqen, eventKey, isSchema } = filesByFqenAndEventKey[idx] if (isSchema) { try { validateSchema(imported) } catch (err: any) { throw new SpruceError({ code: 'INVALID_EVENT_CONTRACT', fullyQualifiedEventName: fqen, brokenProperty: eventKey, originalError: err, }) } } if (!importsByName[fqen]) { importsByName[fqen] = {} } //@ts-ignore importsByName[fqen][eventKey] = imported } Object.keys(importsByName).forEach((fqen) => { const imported = importsByName[fqen] const { eventName } = eventNameUtil.split(fqen) eventSignatures[fqen] = { emitPayloadSchema: buildEmitTargetAndPayloadSchema({ eventName, payloadSchema: imported.emitPayload, targetSchema: imported.emitTarget, }), responsePayloadSchema: imported.responsePayload, emitPermissionContract: imported.emitPermissions, listenPermissionContract: imported.listenPermissions, ...imported.options, } }) didUpdateHandler?.( `Loaded ${Object.keys(eventSignatures).length} local event signatures...` ) if (Object.keys(eventSignatures).length > 0) { const cleaned = eventContractCleanerUtil.cleanPayloadsAndPermissions({ eventSignatures, }) validateEventContract(cleaned) EventStore.localEventCache = cleaned return cleaned } return null } private generateGlobbyForLocalEvents(): string | readonly string[] { return diskUtil.resolvePath( this.cwd, 'src', '**', 'events', '**/*.(builder|options).ts' ) } public async registerEventContract(options: { eventContract: EventContract }) { const client = await this.connectToApi({ shouldAuthAsCurrentSkill: true, }) const results = await client.emit('register-events::v2020_12_25', { payload: { contract: options.eventContract, }, }) eventResponseUtil.getFirstResponseOrThrow(results) EventStore.contractCache = {} return results } public async unRegisterEvents( options: SpruceSchemas.Mercury.v2020_12_25.UnregisterEventsEmitPayload ) { const client = await this.connectToApi({ shouldAuthAsCurrentSkill: true, }) const results = await client.emit('unregister-events::v2020_12_25', { payload: options, }) eventResponseUtil.getFirstResponseOrThrow(results) EventStore.contractCache = {} } } export interface EventStoreFetchEventContractsResponse { errors: SpruceError[] contracts: EventContract[] } type Options = Omit< EventSignature, | 'responsePayloadSchema' | 'emitPayloadSchema' | 'listenPermissionContract' | 'emitPermissionContract' > interface EventImport { options?: Options emitPayload?: Schema emitTarget?: Schema responsePayload?: Schema emitPermissions?: PermissionContract listenPermissions?: PermissionContract } const eventFileNamesImportKeyMap = { 'event.options.ts': { key: 'options', isSchema: false }, 'emitPayload.builder.ts': { key: 'emitPayload', isSchema: true }, 'emitTarget.builder.ts': { key: 'emitTarget', isSchema: true }, 'responsePayload.builder.ts': { key: 'responsePayload', isSchema: true }, 'emitPermissions.builder.ts': { key: 'emitPermissions', isSchema: false }, 'listenPermissions.builder.ts': { key: 'listenPermissions', isSchema: false, }, } export interface FetchContractsOptions { localNamespace?: string namespaces?: string[] didUpdateHandler?: InternalUpdateHandler shouldOnlySyncRemoteEvents?: boolean }