UNPKG

appwrite-database-repository

Version:

288 lines (245 loc) 13 kB
import { Client, Databases, ID, IndexType, Models, Query, QueryTypes, Teams } from 'node-appwrite'; import { ClientWithToken } from './domain/client-with-token'; import { PermissionType } from './domain/permission-type.enum'; import { Document } from './domain/document.type'; const NOT_FOUND_STATUS_CODE = 404; const MAX_PAGE_SIZE = 5000; type PartialDocumentType = Omit<Document, keyof Document>; export class AppwriteRepository<Schema extends PartialDocumentType> { protected databases: Databases; constructor( private readonly client: Client | ClientWithToken, protected readonly databaseId: string, protected readonly collectionId: string, ) { this.databases = new Databases(this.client); } public async listDocuments(queries?: string[]): Promise<Models.DocumentList<Schema & Document>> { return this.databases.listDocuments(this.databaseId, this.collectionId, queries); } public async listByProperties(properties: Partial<Schema>, extraQueries?: string[]): Promise<Models.DocumentList<Schema & Document>> { const queries: string[] = []; for (const [key, value] of Object.entries(properties)) { queries.push(Query.equal(key, value as QueryTypes)); } if (extraQueries) { queries.push(...extraQueries); } return this.databases.listDocuments(this.databaseId, this.collectionId, queries); } public async createDocument(data: Schema, permissions?: string[]): Promise<Schema & Document> { const result = await this.databases.createDocument(this.databaseId, this.collectionId, ID.unique(), data, permissions); return result as Schema & Document; } public async createDocumentWithId(documentId: string, data: Schema, permissions?: string[]): Promise<Schema & Document> { const result = await this.databases.createDocument(this.databaseId, this.collectionId, documentId, data, permissions); return result as Schema & Document; } public async getDocument(documentId: string, queries?: string[]): Promise<(Schema & Document) | null> { try { return await this.databases.getDocument<Schema & Document>(this.databaseId, this.collectionId, documentId, queries); } catch (err: any) { if (err.code === NOT_FOUND_STATUS_CODE) { return null; } throw err; } } public async getDocumentBy(query: Partial<Schema & Document>): Promise<Schema & Document | null> { const queryList: string[] = []; for (const [key, value] of Object.entries(query)) { queryList.push(Query.equal(key, value as QueryTypes)); } queryList.push(Query.limit(1)); const { documents } = await this.databases.listDocuments<Schema & Document>(this.databaseId, this.collectionId, queryList); return documents.length ? documents[0] : null; } public async updateDocument(documentId: string, data?: Partial<Schema>, permissions?: string[]): Promise<Schema & Document> { const result = await this.databases.updateDocument(this.databaseId, this.collectionId, documentId, data, permissions); return result as Schema & Document; } public async deleteDocument(documentId: string): Promise<void> { await this.databases.deleteDocument(this.databaseId, this.collectionId, documentId); } public async createOrUpdateDocument(query: Partial<Schema & Document>, data: Schema, permissions?: string[]): Promise<Schema & Document> { const existingDocument = await this.getDocumentBy(query); if (existingDocument) { return this.updateDocument(existingDocument.$id, data, permissions); } return this.createDocument(data, permissions); } public async createOrUpdateBooleanAttribute(key: string, required: boolean, xdefault?: boolean, array?: boolean): Promise<void> { const exists = await this.attributeExists(key); if (exists) { await this.databases.updateBooleanAttribute(this.databaseId, this.collectionId, key, required, xdefault ?? null as any); } else { await this.databases.createBooleanAttribute(this.databaseId, this.collectionId, key, required, xdefault, array); } } public async createOrUpdateStringAttribute(key: string, size: number, required: boolean, xdefault?: string, array?: boolean, encrypt?: boolean): Promise<void> { const exists = await this.attributeExists(key); if (exists) { await this.databases.updateStringAttribute(this.databaseId, this.collectionId, key, required, xdefault ?? null as any); } else { await this.databases.createStringAttribute(this.databaseId, this.collectionId, key, size, required, xdefault, array, encrypt); } } public async createOrUpdateDateTimeAttribute(key: string, required: boolean, xdefault?: string, array?: boolean): Promise<void> { const exists = await this.attributeExists(key); if (exists) { await this.databases.updateDatetimeAttribute(this.databaseId, this.collectionId, key, required, xdefault ?? null as any); } else { await this.databases.createDatetimeAttribute(this.databaseId, this.collectionId, key, required, xdefault, array); } } public async createOrUpdateEmailAttribute(key: string, required: boolean, xdefault?: string, array?: boolean): Promise<void> { const exists = await this.attributeExists(key); if (exists) { await this.databases.updateEmailAttribute(this.databaseId, this.collectionId, key, required, xdefault ?? null as any); } else { await this.databases.createEmailAttribute(this.databaseId, this.collectionId, key, required, xdefault, array); } } public async createOrUpdateEnumAttribute(key: string, elements: string[], required: boolean, xdefault?: string, array?: boolean): Promise<void> { const exists = await this.attributeExists(key); if (exists) { await this.databases.updateEnumAttribute(this.databaseId, this.collectionId, key, elements, required, xdefault ?? null as any); } else { await this.databases.createEnumAttribute(this.databaseId, this.collectionId, key, elements, required, xdefault, array); } } public async createOrUpdateFloatAttribute(key: string, required: boolean, min?: number, max?: number, xdefault?: number, array?: boolean): Promise<void> { const exists = await this.attributeExists(key); if (exists) { await this.databases.updateFloatAttribute(this.databaseId, this.collectionId, key, required, min ?? null as any, max ?? null as any, xdefault ?? null as any); } else { await this.databases.createFloatAttribute(this.databaseId, this.collectionId, key, required, min, max, xdefault, array); } } public async createOrUpdateIntegerAttribute(key: string, required: boolean, min?: number, max?: number, xdefault?: number): Promise<void> { const exists = await this.attributeExists(key); if (exists) { await this.databases.updateIntegerAttribute(this.databaseId, this.collectionId, key, required, min ?? null as any, max ?? null as any, xdefault ?? null as any); } else { await this.databases.createIntegerAttribute(this.databaseId, this.collectionId, key, required, min, max, xdefault); } } public async createOrUpdateIndex(key: string, type: IndexType, attributes: string[], orders?: string[]): Promise<Models.Index> { try { return await this.databases.getIndex(this.databaseId, this.collectionId, key); } catch (err: any) { if (err.code === NOT_FOUND_STATUS_CODE) { return await this.databases.createIndex(this.databaseId, this.collectionId, key, type, attributes, orders); } console.error(err); return await this.databases.createIndex(this.databaseId, this.collectionId, key, type, attributes, orders); } } public async getIndex(key: string): Promise<Models.Index> { return this.databases.getIndex(this.databaseId, this.collectionId, key); } private async attributeExists(key: string): Promise<boolean> { try { await this.getAttribute(key); } catch (err: any) { if (err.code === NOT_FOUND_STATUS_CODE) { return false; } } return true; } private async getAttribute(key: string): Promise<any> { return this.databases.getAttribute(this.databaseId, this.collectionId, key); } public async deleteAttribute(key: string): Promise<void> { await this.databases.deleteAttribute(this.databaseId, this.collectionId, key); } public async getCollection(): Promise<Models.Collection> { return this.databases.getCollection(this.databaseId, this.collectionId); } public async createOrUpdateCollection(name: string, permissions?: string[], documentSecurity?: boolean, enabled?: boolean): Promise<Models.Collection> { try { await this.databases.getCollection(this.databaseId, this.collectionId); } catch (err: any) { if (err.code === NOT_FOUND_STATUS_CODE) { return this.databases.createCollection(this.databaseId, this.collectionId, name, permissions, documentSecurity, enabled); } console.error(err); } return this.databases.updateCollection(this.databaseId, this.collectionId, name, permissions, documentSecurity, enabled); } public async hasCollectionPermission(client: ClientWithToken, permissionType: PermissionType): Promise<boolean> { const collection = await this.getCollection(); return await this.hasPermission(client, collection.$permissions, permissionType); } public async hasDocumentPermission(client: ClientWithToken, document: Document, permissionType: PermissionType): Promise<boolean> { return this.hasPermission(client, document.$permissions, permissionType); } /* This function checks if the authenticated user has permissions to do a certain action based on a list of permissions Currently non-checked permissions are: - guests permissions, as these are only for guest users without a session. - label permissions, they are not part of the Node.js SDK - user status permissions - member (within team) permissions */ private async hasPermission(client: ClientWithToken, permissionsToCheck: string[], permissionType: PermissionType): Promise<boolean> { const typeIsWritePermission = [PermissionType.Create, PermissionType.Delete, PermissionType.Update].includes(permissionType); if (typeIsWritePermission) { const allowedByWritePermission = permissionsToCheck.some(permission => permission === 'write("any")'); if (allowedByWritePermission) { return true; } } const permissionsOfType = permissionsToCheck.filter((permission) => permission.startsWith(permissionType)); const allowedByAnyPermission = permissionsOfType.some(permission => permission === `${permissionType}("any")`); if (allowedByAnyPermission) { return true; } const userId = client.decodedToken.userId; const allowedByUserPermission = permissionsOfType.some(permission => permission === `${permissionType}("user:${userId}")`); if (allowedByUserPermission) { return true; } const clientTeams = await this.findAllTeams(client); const teamPermissionsWithoutRole = permissionsOfType.filter(permission => permission.includes('team:')); const allowedByTeamPermission = teamPermissionsWithoutRole.some((teamPermission) => { return clientTeams.some(team => teamPermission === `${permissionType}("team:${team.$id}")`); }); if (allowedByTeamPermission) { return true; } const teams = new Teams(this.client); const teamPermissionsWithRole = permissionsOfType.filter(permission => permission.startsWith(`${permissionType}("team:`) && permission.includes('/')); for (const rolePermission of teamPermissionsWithRole) { const teamId = this.getTeamIdFromTeamRolePermission(rolePermission); if (!teamId) { continue; } const memberships = (await teams.listMemberships(teamId, [Query.equal('userId', client.decodedToken.userId)])).memberships; const allMemberRoles = memberships.map(membership => membership.roles).flat(); const hasRolePermission = allMemberRoles.some((role) => rolePermission === `${permissionType}("team:${teamId}/${role}")`); if (hasRolePermission) { return true; } } return false; } private getTeamIdFromTeamRolePermission(permission: string): string | null { const teamSearch = 'team:'; const startIndex = permission.indexOf(teamSearch) + teamSearch.length; const endIndex = permission.indexOf('/'); if (startIndex >= 5 && endIndex !== -1) { return permission.substring(startIndex, endIndex); } else { return null; } } private async findAllTeams(client: ClientWithToken): Promise<Models.Team<{}>[]> { const teams = new Teams(client); return (await teams.list<{}>([Query.limit(MAX_PAGE_SIZE)])).teams; } public asAppClient(client: ClientWithToken): AppwriteRepository<Schema> { return new AppwriteRepository<Schema>(client, this.databaseId, this.collectionId); } }