nestjs-appwrite
Version:
Easier Appwrite integration for your NestJS application.
158 lines (137 loc) • 7.03 kB
text/typescript
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[]);
}