UNPKG

couchdb-web-node-plugin

Version:

A couchdb server, model instance conflict handler, rest api, authentication, session management, schema validator and model relation guarantee for webNode.

483 lines (482 loc) 22.5 kB
import { ChildProcess } from 'child_process'; import { AnyFunction, Mapping, PlainObject, Primitive, ProcessCloseReason, UTILITY_SCOPE } from 'clientnode'; import { Express } from 'express-serve-static-core'; import { IncomingMessage as IncomingHTTPMessage, Server as HTTPServer, ServerResponse as HTTP1ServerResponse } from 'http'; import { Configuration as BaseConfiguration, PluginHandler as BasePluginHandler, ServicePromises as BaseServicePromises, Services as BaseServices, ServicePromisesState as BaseServicePromisesState, ServicesState as BaseServicesState } from 'web-node/type'; export type Attachment = PouchDB.Core.Attachment & { content_type?: PouchDB.Core.Attachment['content_type']; contentType?: PouchDB.Core.Attachment['content_type']; }; export type Attachments = Record<string, Attachment | null>; export type FullAttachment = Omit<PouchDB.Core.FullAttachment, 'data'> & { data?: null | PouchDB.Core.FullAttachment['data']; }; export type StubAttachment = PouchDB.Core.StubAttachment; export type ChangesMeta = PouchDB.Core.ChangesMeta; export type ChangesResponseChange<Type extends object = Mapping<unknown>> = PouchDB.Core.ChangesResponseChange<Type>; export type ChangesStream<Type extends object = Mapping<unknown>> = PouchDB.Core.Changes<Type>; export type ChangesStreamOptions = PouchDB.Core.ChangesOptions; export interface Connection<Type extends object = Mapping<unknown>> extends PouchDB.Database<Type> { installValidationMethods: () => void; bulkDocs<Model>(docs: Array<PutDocument<Type & Model>>, options?: PouchDB.Core.BulkDocsOptions | null): Promise<Array<PouchDB.Core.Response | PouchDB.Core.Error>>; post<Model>(doc: PostDocument<Type & Model>, options?: PouchDB.Core.Options | null): Promise<PouchDB.Core.Response>; put<Model>(doc: PutDocument<Type & Model>, options?: PouchDB.Core.PutOptions | null): Promise<PouchDB.Core.Response>; } export type Connector = PouchDB.Static; export type DatabaseConnectorConfiguration = PouchDB.Configuration.RemoteDatabaseConfiguration; export type DatabaseError = PouchDB.Core.Error; export type DatabaseFetch = PouchDB.Core.Options['fetch']; export type DatabaseResponse = PouchDB.Core.Response; export type Document<Type extends object = PlainObject> = PouchDB.Core.Document<Type>; export type ExistingDocument<Type extends object = PlainObject> = PouchDB.Core.ExistingDocument<Type>; export type DocumentGetMeta = PouchDB.Core.GetMeta; export type DocumentIDMeta = PouchDB.Core.IdMeta; export type DocumentRevisionIDMeta = PouchDB.Core.RevisionIdMeta; export type FindRequest<Type extends object> = PouchDB.Find.FindRequest<Type>; export type FindResponse<Type extends object> = PouchDB.Find.FindResponse<Type>; export type DeleteIndexOptions = PouchDB.Find.DeleteIndexOptions; export type GetOptions = PouchDB.Core.GetOptions; export type PutOptions = PouchDB.Core.Options; export type PostDocument<Type extends object> = Omit<PouchDB.Core.PostDocument<Type>, 'attachments'> & { _attachments?: Attachments | null; }; export type PutDocument<Type extends object> = Omit<PouchDB.Core.PutDocument<Type>, 'attachments'> & { _attachments?: Attachments | null; }; export type Index = PouchDB.Find.Index; export type DatabasePlugin = AnyFunction; export type AllowedRoles = (Array<string> | string | { read?: Array<string> | string; write?: Array<string> | string; }); export interface NormalizedAllowedRoles { read: Array<string>; write: Array<string>; } export interface NormalizedAllowedModelRoles extends NormalizedAllowedRoles { properties: Mapping<NormalizedAllowedRoles>; } export type AllowedModelRolesMapping = Mapping<NormalizedAllowedModelRoles>; export interface Constraint { description?: string; evaluation: string; } export declare const PrimitiveTypes: readonly ["boolean", "DateTime", "integer", "number", "string"]; export type PrimitiveType = typeof PrimitiveTypes[number]; export type Type = boolean | number | string; export type TypeSpecification = Array<Type> | Array<Array<Type>> | Type; export type ConstraintKey = 'arrayConstraintExecution' | 'arrayConstraintExpression' | 'conflictingConstraintExecution' | 'conflictingConstraintExpression' | 'constraintExecution' | 'constraintExpression'; export interface SelectionMapping { label: string; value: unknown; } export type Pattern = Array<RegExp | string> | RegExp | string; export interface BasePropertySpecification<Type, AdditionalSpecifications extends object> { allowedRoles?: AllowedRoles; ignoreFillUp?: boolean; arrayConstraintExecution?: Constraint; arrayConstraintExpression?: Constraint; conflictingConstraintExecution?: Constraint; conflictingConstraintExpression?: Constraint; constraintExecution?: Constraint; constraintExpression?: Constraint; onCreateExecution?: string; onCreateExpression?: string; onUpdateExecution?: string; onUpdateExpression?: string; pattern?: Pattern; invertedPattern?: Pattern; contentTypePattern?: Pattern; invertedContentTypePattern?: Pattern; maximum?: number; minimum?: number; maximumAggregatedSize?: number; minimumAggregatedSize?: number; maximumLength?: number; minimumLength?: number; maximumNumber?: number; minimumNumber?: number; maximumSize?: number; minimumSize?: number; mutable?: boolean; nullable?: boolean; writable?: boolean; selection?: Array<SelectionMapping> | Array<unknown> | Mapping<unknown>; type?: TypeSpecification; emptyEqualsNull?: boolean; trim?: boolean; name?: string; declaration?: string; description?: string; index?: boolean; oldName?: Array<string> | string; value?: Type; additionalSpecifications?: AdditionalSpecifications; } export interface PropertySpecification<Type = unknown, AdditionalSpecifications extends object = object> extends BasePropertySpecification<Type, AdditionalSpecifications> { default?: Type; } export interface FileSpecification<Type extends Attachment = Attachment, AdditionalSpecifications extends object = object> extends BasePropertySpecification<Type, AdditionalSpecifications> { default?: Mapping<Type>; fileName?: PropertySpecification<string, AdditionalSpecifications>; } export interface BaseModel<AttachmentType extends Attachment = Attachment, AdditionalSpecifications extends object = object, AdditionalPropertiesType = unknown> { _additional?: PropertySpecification<AdditionalPropertiesType, AdditionalSpecifications>; _allowedRoles?: AllowedRoles; _attachments?: Mapping<FileSpecification<AttachmentType, AdditionalSpecifications>> | null; _constraintExecutions?: Array<Constraint> | Constraint; _constraintExpressions?: Array<Constraint> | Constraint; _createExecution?: string; _createExpression?: string; _extends?: Array<string> | string; _maximumAggregatedSize?: number; _minimumAggregatedSize?: number; _oldType?: Array<string> | string; _onUpdateExecution?: string; _onUpdateExpression?: string; _id: PropertySpecification<string, AdditionalSpecifications>; _rev: PropertySpecification<string, AdditionalSpecifications>; } export type Model<Type extends object | undefined = object, AttachmentType extends Attachment = Attachment, AdditionalSpecifications extends object = object, AdditionalPropertiesType = unknown> = BaseModel<AttachmentType, AdditionalSpecifications, AdditionalPropertiesType> & { [Property in keyof Type]: PropertySpecification<Type[Property] extends Array<unknown> ? (Type[Property][number] extends object | undefined ? Array<Model<Type[Property][number], AttachmentType, AdditionalSpecifications, AdditionalPropertiesType>> : Type[Property]) : Type[Property] extends object | undefined ? (Type[Property] extends Date | undefined ? Type[Property] : Model<Type[Property], AttachmentType, AdditionalSpecifications, AdditionalPropertiesType>) : Type[Property], AdditionalSpecifications>; }; export type Models<Type extends object = object, AttachmentType extends Attachment = Attachment, AdditionalSpecifications extends object = object, AdditionalPropertiesType = unknown> = Mapping<Model<Type, AttachmentType, AdditionalSpecifications, AdditionalPropertiesType>>; export type UpdateStrategy = 'fillUp' | 'incremental' | 'migrate' | 'replace'; export type DocumentContent = Array<DocumentContent> | PlainObject | Primitive; export interface DocumentStrategyMeta { _updateStrategy?: UpdateStrategy; } export interface DocumentTypeMeta { '-type': string; } export type BaseDocument = Omit<ChangesMeta, '_attachments'> & Omit<DocumentGetMeta, '_attachments'> & DocumentIDMeta & DocumentRevisionIDMeta & DocumentStrategyMeta & DocumentTypeMeta & { _attachments?: Attachments | null; }; export type FullDocument<Type extends object = object, AdditionalPropertyTypes = unknown> = BaseDocument & Document<Type> & Mapping<AdditionalPropertyTypes>; export type PartialFullDocument<Type extends object = object, AdditionalPropertyTypes = unknown> = Partial<BaseDocument> & Partial<Document<Type>> & Mapping<AdditionalPropertyTypes>; export interface SpecialPropertyNames { additional: '_additional'; allowedRole: '_allowedRoles'; attachment: '_attachments'; conflict: '_conflicts'; deleted: '_deleted'; deletedConflict: '_deleted_conflict'; extend: '_extends'; id: '_id'; revision: '_rev'; revisions: '_revisions'; revisionsInformation: '_revs_info'; strategy: '_updateStrategy'; type: '-type'; constraint: { execution: '_constraintExecutions'; expression: '_constraintExpressions'; }; create: { execution: '_createExecution'; expression: '_createExecution'; }; designDocumentNamePrefix: string; localSequence: '_local_seq'; maximumAggregatedSize: '_maximumAggregatedSize'; minimumAggregatedSize: '_minimumAggregatedSize'; oldType: '_oldType'; update: { execution: '_onUpdateExecution'; expression: '_onUpdateExpression'; }; } export interface PropertyNameConfiguration { reserved: Array<string>; special: SpecialPropertyNames; typePattern: { private: string; public: string; }; validatedDocumentsCache: string; } export interface BaseModelConfiguration<Type, AdditionalSpecifications extends object> { dateTimeFormat: 'iso' | 'iso8601' | 'number'; property: { defaultSpecification: PropertySpecification<Type, AdditionalSpecifications>; name: PropertyNameConfiguration; }; updateStrategy: UpdateStrategy; } export interface ModelConfiguration<Type extends object = object, AttachmentType extends Attachment = Attachment, AdditionalSpecifications extends object = object, AdditionalPropertiesType = unknown> extends BaseModelConfiguration<Type, AdditionalSpecifications> { autoMigrationPath: string; entities: Models<Type, AttachmentType, AdditionalSpecifications, AdditionalPropertiesType>; triggerInitialCompaction: boolean; updateConfiguration: boolean; updateValidation: boolean; } export interface UserContext { db: string; name?: string; roles: Array<string>; } export interface DatabaseUserConfiguration { names?: Array<string>; roles?: Array<string>; } export interface Runner { adminUserConfigurationPath: string; configurationFile?: null | { content: string; path: string; }; names: Array<string> | string; } export interface BinaryRunner extends Runner { arguments?: Array<string> | null | string; binaryFilePath?: null | string; environment?: null | Mapping; locations: Array<string> | string; } export interface InPlaceRunner extends Runner { packages: Array<string> | string; } export interface SecuritySettings { admins?: DatabaseUserConfiguration; members?: DatabaseUserConfiguration; _validatedDocuments?: Set<string>; } export type AdvancedFetchOptions = RequestInit & { timeout?: number; }; export type ConnectorConfiguration = DatabaseConnectorConfiguration & { fetch?: AdvancedFetchOptions | null; fetchInterceptor: { numberOfRetries: number; retryIntervalInSeconds: number; exponentialBackoff: boolean; maximumRetryIntervallInSeconds: number; }; }; export interface CoreConfiguration<Type extends object = Mapping<unknown>, AttachmentType extends Attachment = Attachment, AdditionalSpecifications extends object = object, AdditionalPropertiesType = unknown> { attachAutoRestarter: boolean; backend: { configuration: PlainObject; prefixes: Array<string>; }; runner: { memoryInMegaByte: string; nodePath: string; variants: Array<BinaryRunner | InPlaceRunner>; }; changesStream: ChangesStreamOptions; changesStreamReinitializer: { retries: number; retryWaitingFactorInSeconds: number; maximumRetryWaitingTimeInSeconds: number; }; numberOfParallelChangesRunner: number; updateMaterializedViewsChangesStream: ChangesStreamOptions; connector: ConnectorConfiguration; security: { _default: SecuritySettings; _users: SecuritySettings; [key: string]: SecuritySettings; }; createGenericFlatIndex: boolean; databaseName: string; debug: boolean; ensureAdminPresence: boolean; ensureSecuritySettingsPresence: boolean; ensureUserPresence: boolean; ignoreNoChangeError: boolean; local: boolean; maximumNumberOfEntitiesInBulkOperation: number; maximumRepresentationLength: number; maximumRepresentationTryLength: number; model: ModelConfiguration<Type, AttachmentType, AdditionalSpecifications, AdditionalPropertiesType>; path: string; skipLatestRevisionDetermining: boolean; url: string; admin: { name: string; password: string; }; users: Mapping<{ name: string; password: string; roles: Array<string>; }>; materializedViews: Mapping<Mapping<{ query: PouchDB.Find.FindRequest<object>; initialMapperExpression?: string; updateExpression?: string; }>>; } export type Configuration<ConfigurationType = Mapping<unknown>> = BaseConfiguration<{ couchdb: CoreConfiguration; }> & ConfigurationType; export interface CouchDB<Type extends object = Mapping<unknown>> { changesStream: ChangesStream; lastChangesSequenceIdentifier?: number | string; updateMaterializedViewsChangesStream?: ChangesStream; lastUpdateMaterializedViewsChangesSequenceIdentifier?: number | string; connection: Connection<Type>; connector: Connector; server: { expressInstance?: Express; expressPouchDBInstance?: Express; process?: ChildProcess | HTTPServer; reject: (value: Error | ProcessCloseReason) => void; resolve: (reason?: ProcessCloseReason) => void; restart: (state: State) => Promise<void>; start: (state: State) => Promise<void>; stop: (services: Services, configuration: Configuration) => Promise<void>; runner: BinaryRunner | InPlaceRunner; }; validateDocument: (document: FullDocument, options: { checkPublicModelType?: boolean; type?: string; oldDocument?: FullDocument; }) => Error | true; } export type ServicePromises<Type = Mapping<unknown>> = BaseServicePromises<{ couchdb: Promise<ProcessCloseReason>; }> & Type; export type Services<Type = Mapping<unknown>> = BaseServices<{ couchdb: CouchDB; }> & Type; export type ServicesState<Type = undefined> = BaseServicesState<Type, Configuration, Services>; export type State<Type = undefined> = BaseServicePromisesState<Type, Configuration, Services, ServicePromises>; export interface PluginHandler extends BasePluginHandler { /** * Hook after changes stream initialization. * @param state - Application state. * @returns Promise resolving to nothing. */ couchdbInitializeChangesStream?(state: State<ChangesStream>): Promise<void>; /** * Hook after each data change. * @param state - Application state. * @returns Promise resolving to nothing. */ couchdbChange?(state: State<ChangesResponseChange>): Promise<void>; /** * Hook after each database restart. * @param state - Application state. * @returns Promise resolving to nothing. */ restartCouchdb?(state: State): Promise<void>; /** * Hook before registering pouchdb routes into the express server * instance. * @param state - Application state. * @returns Promise resolving to find result. */ onPouchDBFind?(state: State<{ request: IncomingHTTPMessage & { body: FindRequest<PlainObject>; }; response: HTTP1ServerResponse; }>): Promise<FindResponse<object> | undefined>; /** * Hook before registering pouchdb routes into the express server * instance. * @param state - Application state. * @returns Promise resolving to nothing. */ initializeExpressPouchDB?(state: State<{ expressInstance: Express; expressPouchDBInstance: Express; }>): Promise<void>; } export interface EmptyEvaluationErrorData { empty: string; } export interface EvaluationErrorData<S = Mapping<unknown>> { code: string; error: unknown; scope: S; } export type EvaluationError = DatabaseError & EvaluationErrorData; export interface CompilationErrorData<S = Mapping<unknown>> extends EvaluationErrorData<S> { compilation: string; } export interface RuntimeErrorData<S = Mapping<unknown>> extends EvaluationErrorData<S> { runtime: string; } export interface BasicScope<Type extends object, AttachmentType extends Attachment, AdditionalSpecifications extends object, AdditionalPropertiesType> { attachmentWithPrefixExists: (namePrefix: string) => boolean; checkDocument: (newDocument: PartialFullDocument<Type, AdditionalPropertiesType>, oldDocument: PartialFullDocument<Type, AdditionalPropertiesType> | null, parentNames: Array<string>) => CheckedDocumentResult<Type, AdditionalPropertiesType>; getDateTime: (value: number | string) => Date; getEffectiveValue: (name: string, newDocument: PartialFullDocument<Type, AdditionalPropertiesType>, oldDocument: (null | PartialFullDocument<Type, AdditionalPropertiesType>)) => unknown; getFileNameByPrefix: (prefix?: string, attachments?: Mapping<AttachmentType>) => null | string; isDefinedPropertyValue: (name: keyof object, document: PartialFullDocument<Type, AdditionalPropertiesType>) => boolean; require: null | typeof require; serialize: (value: unknown) => string; id: string; revision: string; idName: string; revisionName: string; specialNames: SpecialPropertyNames; typeName: string; modelConfiguration: BaseModelConfiguration<Type, AdditionalSpecifications>; models: Models<Type, AttachmentType, AdditionalSpecifications, AdditionalPropertiesType>; now: Date; nowUTCTimestamp: number; securitySettings: Partial<SecuritySettings>; updateStrategy: UpdateStrategy; userContext: Partial<UserContext>; } export interface CommonScope<ObjectType extends object, Type, AttachmentType extends Attachment, AdditionalSpecifications extends object, AdditionalPropertiesType> { checkPropertyContent: (newValue: Type, name: string, propertySpecification: PropertySpecification<Type, AdditionalSpecifications>, oldValue: Type) => CheckedPropertyResult<Type>; model: Model<ObjectType, AttachmentType, AdditionalSpecifications, AdditionalPropertiesType>; modelName: string; type: Array<string> | string; newDocument: PartialFullDocument<ObjectType, AdditionalPropertiesType>; oldDocument: null | PartialFullDocument<ObjectType, AdditionalPropertiesType>; parentNames: Array<string>; pathDescription: string; } export interface PropertyScope<ObjectType extends object, Type, PropertyType, AttachmentType extends Attachment, AdditionalSpecifications extends object, AdditionalPropertiesType> extends CommonScope<ObjectType, PropertyType, AttachmentType, AdditionalSpecifications, AdditionalPropertiesType> { name: string; newValue: Type; oldValue?: Type; propertySpecification: PropertySpecification<Type, AdditionalSpecifications>; attachmentsTarget?: Mapping<AttachmentType | null>; } export interface EvaluationResult<ObjectType extends object, Type, PropertyType, AttachmentType extends Attachment, AdditionalSpecifications extends object, AdditionalPropertiesType, Scope = (BasicScope<ObjectType, AttachmentType, AdditionalSpecifications, AdditionalPropertiesType> & CommonScope<ObjectType, PropertyType, AttachmentType, AdditionalSpecifications, AdditionalPropertiesType>)> { code: string; result: Type; scope: Scope; } export type Evaluate<R, P extends Array<unknown>> = (...parameters: P) => R; export interface CheckedResult { changedPath: Array<string>; } export interface CheckedPropertyResult<Type> extends CheckedResult { newValue?: null | Type; } export interface CheckedDocumentResult<ObjectType extends object, AdditionalPropertiesType> extends CheckedResult { newDocument: PartialFullDocument<ObjectType, AdditionalPropertiesType>; } export type Migrator<Type extends object = object, AttachmentType extends Attachment = Attachment, AdditionalSpecifications extends object = object, AdditionalPropertiesType = unknown> = (document: Document, scope: (typeof UTILITY_SCOPE & { configuration: Configuration; databaseHelper: Mapping<unknown>; idName: string; typeName: string; migrators: Mapping<Migrator<Type, AttachmentType, AdditionalSpecifications, AdditionalPropertiesType>>; models: Models<Type, AttachmentType, AdditionalSpecifications, AdditionalPropertiesType>; modelConfiguration: ModelConfiguration<Type, AttachmentType, AdditionalSpecifications, AdditionalPropertiesType>; selfFilePath: string; services: Services; })) => Document | null; export type DateRepresentationType = Date | null | number | string; export type User = BaseDocument & { password: string; roles: Array<string>; }; export type MaterializedViewDocument = BaseDocument & Record<string, unknown>; export interface Interval { end: number | string; start: number | string; } export interface Location { latitude: number; longitude: number; }