UNPKG

nestjs-appwrite

Version:

Easier Appwrite integration for your NestJS application.

158 lines (137 loc) 7.03 kB
import { Logger, Provider } from '@nestjs/common'; import { AsyncModelFactory } from './interface/async-model-factory.interface'; import typeMetadataStorage from './storage/type-metadata.storage'; import { CLIENT_PROVIDER_NAME, CONFIG_PROVIDER_NAME } from './appwrite.constants'; import { Client, Databases, IndexType, Models, Query } from 'node-appwrite'; import { AppwriteRepository } from 'appwrite-database-repository'; import { Config } from './config/config.interface'; import { AppwriteAttribute } from './interface/appwrite-attribute.interface'; import { DatabaseResourceStatus } from './domain/attribute-status.enum'; const MAX_PAGE_SIZE = 5000; const HALF_SECOND_IN_MS = 500; const logger = new Logger('appwrite.providers'); type AttributeType = Models.AttributeInteger | Models.AttributeBoolean | Models.AttributeEnum | Models.AttributeEmail | Models.AttributeDatetime | Models.AttributeFloat | Models.AttributeString; const setupAttributes = async (option: AsyncModelFactory, repository: AppwriteRepository<Object>, config: Config): Promise<void> => { const classProperties = typeMetadataStorage.getClassProperties(option.class); const currentAttributes = (await repository.getCollection()).attributes as unknown as AttributeType[]; await Promise.all(classProperties.map(async (prop) => { const { options } = prop; try { switch (prop.options.type) { case String: { if (options.enum) { return await repository.createOrUpdateEnumAttribute(prop.propertyKey, Object.values(prop.options.enum as any), options.required, options.default, options.isArray); } if (options.isEmail) { return await repository.createOrUpdateEmailAttribute(prop.propertyKey, options.required, options.default, options.isArray); } return await repository.createOrUpdateStringAttribute(prop.propertyKey, options.size as number, options.required, options.default, options.isArray, options.encrypt); } case Number: { if (options.isInt) { return await repository.createOrUpdateIntegerAttribute(prop.propertyKey, options.required, options.min, options.max, options.default); } return await repository.createOrUpdateFloatAttribute(prop.propertyKey, options.required, options.min, options.max, options.default); } case Boolean: { return await repository.createOrUpdateBooleanAttribute(prop.propertyKey, options.required, options.default, options.isArray); } case Date: { return await repository.createOrUpdateDateTimeAttribute(prop.propertyKey, options.required, options.default, options.isArray); } default: { logger.warn(`Property type ${prop.options.type} is not a valid Appwrite property type`); } } } catch (err) { logger.log(`Caught err for attribute ${prop.propertyKey} of type ${prop.options.type}}`); logger.error(err); } return; })); if (!config.DELETE_REMOVED_ATTRIBUTES) { return; } const collection = await repository.getCollection(); const collectionAttributes = collection.attributes as unknown as AppwriteAttribute[]; const attributeKeys = classProperties.map(property => property.propertyKey); const deletedAttributes = currentAttributes.filter(attribute => !attributeKeys.includes(attribute.key)); await Promise.all(deletedAttributes.map(async (attribute) => { const attributeFound = collectionAttributes.find(collectionAttribute => attribute.key === collectionAttribute.key); if (attributeFound?.status === DatabaseResourceStatus.Processing) { return; } try { return await repository.deleteAttribute(attribute.key); } catch (err) { logger.log(`Caught err when deleting attribute with key ${attribute.key}`); logger.error(err); } })) }; const waitForAttributesToBeCreated = async (client: Client, config: Config, collectionId: string): Promise<void> => { const databases = new Databases(client); let attributesCreating = true; while (attributesCreating) { const attributes = (await databases.listAttributes(config.APPWRITE_DATABASE_ID, collectionId, [Query.limit(MAX_PAGE_SIZE)])).attributes as unknown as AppwriteAttribute[]; attributesCreating = attributes.some((attribute) => attribute.status === DatabaseResourceStatus.Processing); if (attributesCreating) { logger.log('Waiting for attribute to be created'); await new Promise(resolve => setTimeout(resolve, HALF_SECOND_IN_MS)); } } }; const setupIndexes = async (option: AsyncModelFactory, repository: AppwriteRepository<Object>): Promise<void> => { const classIndexes = typeMetadataStorage.getClassIndexes(option.class); await Promise.all(classIndexes.map(async (index) => { const { options } = index; const indexName = `${index.propertyKey}-${options.type}`; try { let existingIndex = await repository.createOrUpdateIndex(indexName, options.type as IndexType, options.attributes, options.orders); let indexCreating = existingIndex.status === DatabaseResourceStatus.Processing; while (indexCreating) { await new Promise(resolve => setTimeout(resolve, HALF_SECOND_IN_MS)); existingIndex = await repository.getIndex(indexName); indexCreating = existingIndex.status === DatabaseResourceStatus.Processing; } } catch (err) { logger.log(`Caught err for index ${indexName} of type ${options.type}}`); logger.error(err); } })); }; export function createAppwriteAsyncProviders( modelFactories: AsyncModelFactory[] = [], ): Provider[] { return modelFactories.reduce((providers, option) => { return [ ...providers, { provide: option.class.name, useFactory: async (client: Client, config: Config): Promise<AppwriteRepository<Object>> => { const { name } = option.class; logger.debug(`Initializing schema: ${name}`); const schemaMetaData = typeMetadataStorage.getSchemaMetadata(option.class); const repository = new AppwriteRepository( client, config.APPWRITE_DATABASE_ID, schemaMetaData.collectionId, ); try { await repository.createOrUpdateCollection(schemaMetaData.collectionName, schemaMetaData.permissions, schemaMetaData.documentSecurity, schemaMetaData.enabled); } catch (err) { logger.log(`Caught err for update collection ${schemaMetaData.collectionName}`); logger.error(err); } await setupAttributes(option, repository, config); await waitForAttributesToBeCreated(client, config, schemaMetaData.collectionId); await setupIndexes(option, repository); logger.debug(`Done initializing schema: ${name}`); return repository; }, inject: [CLIENT_PROVIDER_NAME, CONFIG_PROVIDER_NAME, ...(option.inject || [])], }, ]; }, [] as Provider[]); }