appwrite-database-repository
Version:
288 lines (245 loc) • 13 kB
text/typescript
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);
}
}