UNPKG

appwrite-server-wrapper

Version:

Wrapper library to handle Appwrite methods including server handling using SSR with NextJS v15 (useActionState, useAction,...)

1,255 lines 67.4 kB
"use server"; import { getSchema, attributesEqual, createAttribute, updateAttribute, //getAttributeFromKey, } from "../collections"; import { handleApwError } from "../exceptions"; import { createAdminClient } from "../appwriteClients"; import { ID, Query, } from "node-appwrite"; import { databaseId, usersCollectionId } from "../appwriteConfig"; import { generateMigrationId, toLogs } from "../ssr-utils"; import { isCollectionSchema, schemaToFile } from "../collections/schema"; const createBooleanAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const createBooleanAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, newArgs.required, newArgs.xdefault, newArgs.array, ]; const data = await databases.createBooleanAttribute(...createBooleanAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createCollection = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? ID.unique(); const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const createCollectionParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.name, newArgs.permissions, newArgs.documentSecurity, newArgs.enabled, ]; const data = await databases.createCollection(...createCollectionParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createCollectionWithSchema = async (args) => { // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? ID.unique(); // Initialize a log object. const logTopic = "migration"; const logDetails = "schemaCreate"; const logContent = { id: await generateMigrationId(args.name), executed_at: new Date().toISOString(), status: "success", databaseId: finalDatabaseId, collectionId: finalCollectionId, changes: [], }; try { const { databases } = await createAdminClient(); const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; logContent.changes.push({ action: "listCollections", information: `Listing collections in database '${newArgs.databaseId}'.`, }); const collList = await databases.listCollections(newArgs.databaseId); let coll = collList.collections.find((collection) => collection.name === newArgs.name); if (coll) { logContent.changes.push({ action: "listCollections", information: `Collection '${newArgs.name}' already exists.`, }); throw new Error(`Collection '${newArgs.name}' already exists`); } else { logContent.changes.push({ action: "listCollections", information: `Collection '${newArgs.name}' not found; proceeding to create.`, }); const schema = await getSchema(newArgs.name, logContent); if (!schema || !isCollectionSchema(schema)) { logContent.changes.push({ action: "getSchema", information: `No valid schema found for collection '${newArgs.name}'.`, }); throw new Error(`No schema found for collection '${newArgs.name}'`); } if (!schema.attributes || schema.attributes.length < 1) { logContent.changes.push({ action: "getSchema", information: `No attributes found in schema '${schema.collectionName}'.`, }); throw new Error(`No attributes found in schema for collection '${newArgs.name}'`); } logContent.changes.push({ action: "getSchema", information: `Schema '${schema.collectionName}' loaded for collection creation.`, }); coll = await databases.createCollection(newArgs.databaseId, newArgs.collectionId, schema.collectionName, schema.permissions, schema.documentSecurity, schema.enabled); logContent.changes.push({ action: "createCollection", information: `Collection '${schema.collectionName}' created.`, }); for (const attr of schema.attributes) { logContent.changes.push({ action: "createAttribute", information: `Creating attribute '${attr.key}'.`, }); // Validate that xdefault is not defined if required is set to true const hasRequiredTrue = "required" in attr && attr.required === true; const hasXdefault = attr.type !== "relationship" && "xdefault" in attr && attr.xdefault !== undefined; if (hasRequiredTrue && hasXdefault) { const msg = `Not able to create Attribute '${attr.key}' for Collection '${newArgs.name}' (id: '${newArgs.collectionId}'), because both 'xdefault' and 'required: true' are set. Appwrite does not allow xdefault to be defined while required is true.`; logContent.changes.push({ action: "createAttribute", information: msg, }); throw new Error(msg); } // Check relationshipAttributeData prop if (attr.key === "relationship") { if (newArgs.relationshipAttributeData?.length) { newArgs.relationshipAttributeData.map((relAttr) => { if (!relAttr.relationshipKey.length || !relAttr.relatedCollectionId.length) { logContent.changes.push({ action: "createAttribute (relationship)", information: `Not able to create Attribute '${newArgs.name}' (id: '${newArgs.collectionId}'), since relationship information for key '${attr.key}' is missing (relationshipKey or relatedCollectionId or both, passed with param 'relationshipAttributeData' are are not valid)`, }); throw new Error(`Not able to create Attribute '${newArgs.name}' (id: '${newArgs.collectionId}'), since relationship information for key '${attr.key}' is missing (relationshipKey or relatedCollectionId or both, passed with param 'relationshipAttributeData' are are not valid)`); } }); } else { logContent.changes.push({ action: "createAttribute (relationship)", information: `Not able to create Attribute '${newArgs.name}' (id: '${newArgs.collectionId}'), since relationship information for key '${attr.key}' is missing (relationshipAttributeData)`, }); throw new Error(`Not able to create Attribute '${newArgs.name}' (id: '${newArgs.collectionId}'), since relationship information for key '${attr.key}' is missing (relationshipAttributeData)`); } } try { await createAttribute(newArgs.databaseId, newArgs.collectionId, attr, newArgs.relationshipAttributeData?.find((relAttr) => relAttr.relationshipKey === attr.key)?.relatedCollectionId); logContent.changes.push({ action: "createAttribute", information: `Attribute '${attr.key}' created.`, }); } catch (error) { throw new Error(`Couldn't create attribute '${attr.key}': ${error}`); } } for (const index of schema.indexes) { logContent.changes.push({ action: "createIndex", information: `Creating index '${index.key}' of type '${index.type}'.`, }); try { await databases.createIndex(newArgs.databaseId, newArgs.collectionId, index.key, index.type, index.attributes, index.orders); logContent.changes.push({ action: "createIndex", information: `Index '${index.key}' created.`, }); } catch (error) { throw new Error(`Couldn't create index '${index.key}': ${error}`); } } logContent.executed_at = new Date().toISOString(); logContent.status = "success"; await toLogs(logTopic, logDetails, logContent); return { data: coll, error: null }; } } catch (error) { logContent.executed_at = new Date().toISOString(); logContent.status = "failure"; await toLogs(logTopic, logDetails, logContent); return { data: null, error: await handleApwError({ error }), }; } }; const createDatabase = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const newArgs = { ...args, databaseId: finalDatabaseId, }; const createDatabaseParams = [ newArgs.databaseId, newArgs.name, newArgs.enabled, ]; const data = await databases.create(...createDatabaseParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createDatetimeAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const createDatetimeAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, newArgs.required, newArgs.xdefault, newArgs.array, ]; const data = await databases.createDatetimeAttribute(...createDatetimeAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createDocument = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, documentId: ID.unique(), }; const createDocumentParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.documentId, newArgs.data, newArgs.permissions, ]; const data = await databases.createDocument(...Object.values(newArgs)); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createEmailAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const createEmailAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, newArgs.required, newArgs.xdefault, newArgs.array, ]; const data = await databases.createEmailAttribute(...createEmailAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createEnumAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const createEnumAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, newArgs.elements, newArgs.required, newArgs.xdefault, newArgs.array, ]; const data = await databases.createEnumAttribute(...createEnumAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createFloatAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const createFloatAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, newArgs.required, newArgs.min, newArgs.max, newArgs.xdefault, newArgs.array, ]; const data = await databases.createFloatAttribute(...createFloatAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createIndex = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const createIndexParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, newArgs.type, newArgs.attributes, newArgs.orders, ]; const data = await databases.createIndex(...createIndexParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createIntegerAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const createIntegerAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, newArgs.required, newArgs.min, newArgs.max, newArgs.xdefault, newArgs.array, ]; const data = await databases.createIntegerAttribute(...createIntegerAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createIpAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const createIpAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, newArgs.required, newArgs.xdefault, newArgs.array, ]; const data = await databases.createIpAttribute(...createIpAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createRelationshipAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const createRelationshipAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.relatedCollectionId, newArgs.type, newArgs.twoWay, newArgs.key, newArgs.twoWayKey, newArgs.onDelete, ]; const data = await databases.createRelationshipAttribute(...createRelationshipAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createStringAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const createStringAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, newArgs.size, newArgs.required, newArgs.xdefault, newArgs.array, newArgs.encrypt, ]; const data = await databases.createStringAttribute(...createStringAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const createUrlAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const createUrlAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, newArgs.required, newArgs.xdefault, newArgs.array, ]; const data = await databases.createUrlAttribute(...createUrlAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const deleteAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const deleteAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, ]; const data = await databases.deleteAttribute(...deleteAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const deleteCollection = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const newArgs = { ...args, databaseId: finalDatabaseId, }; const deleteCollectionParams = [ newArgs.databaseId, newArgs.collectionId, ]; const data = await databases.deleteCollection(...deleteCollectionParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const deleteDatabase = async ({ ...args }) => { try { const { databases } = await createAdminClient(); const newArgs = { ...args, }; const deleteDatabaseParams = [newArgs.databaseId]; const data = await databases.delete(...deleteDatabaseParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const deleteDocument = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const deleteDocumentParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.documentId, ]; const data = await databases.deleteDocument(...deleteDocumentParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const deleteIndex = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const deleteIndexParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, ]; const data = await databases.deleteIndex(...deleteIndexParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const getAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const getAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, ]; const data = await databases.getAttribute(...getAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const getCollection = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const newArgs = { ...args, databaseId: finalDatabaseId, }; const getCollectionParams = [ newArgs.databaseId, newArgs.collectionId, ]; const data = await databases.getCollection(...getCollectionParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const getDatabase = async ({ ...args }) => { try { const { databases } = await createAdminClient(); const newArgs = { ...args, }; const getDatabaseParams = [newArgs.databaseId]; const data = await databases.get(...getDatabaseParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const getDocument = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const getDocumentParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.documentId, newArgs.queries, ]; const data = await databases.getDocument(...getDocumentParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const getIndex = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const getIndexParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, ]; const data = await databases.getIndex(...getIndexParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const listAttributes = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const listAttributesParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.queries, ]; const data = await databases.listAttributes(...listAttributesParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const listCollections = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const newArgs = { ...args, databaseId: finalDatabaseId, }; const listCollectionsParams = [ newArgs.databaseId, newArgs.queries, newArgs.search, ]; const data = await databases.listCollections(...listCollectionsParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const listDatabases = async ({ ...args }) => { try { const { databases } = await createAdminClient(); const newArgs = { ...args, }; const listDatabasesParams = [ newArgs.queries, newArgs.search, ]; const data = await databases.list(...listDatabasesParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const listDocuments = async (args) => { try { const { databases } = await createAdminClient(); const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const listDocumentsParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.queries, ]; const data = await databases.listDocuments(...listDocumentsParams); // Defensive null check if (!data || !Array.isArray(data.documents)) { return { data: { documents: [], total: 0 }, error: null, }; } let filteredDocuments = data.documents; const getMatchingId = (val) => { if (val === null || val === undefined) return null; if (typeof val === "object" && "$id" in val) return val.$id; if (typeof val === "string") return val; return null; }; const matchesValue = (docVal, target) => { if (Array.isArray(docVal)) { return docVal.some((item) => getMatchingId(item) === target); } return getMatchingId(docVal) === target; }; const matchesValues = (docVal, targets) => { if (Array.isArray(docVal)) { return docVal.some((item) => targets.includes(getMatchingId(item))); } const docId = getMatchingId(docVal); return docId ? targets.includes(docId) : false; }; if (newArgs.relationshipQueries?.length) { filteredDocuments = data.documents.filter((doc) => { return newArgs.relationshipQueries.every((query) => { try { const parsedQuery = JSON.parse(query); const evaluateQuery = (parsedQuery, doc) => { const { attribute, method, values } = parsedQuery; switch (method) { case "equal": return matchesValue(doc[attribute], values[0]); case "notEqual": return !matchesValue(doc[attribute], values[0]); case "lessThan": return getMatchingId(doc[attribute]) < values[0]; case "lessThanEqual": return getMatchingId(doc[attribute]) <= values[0]; case "greaterThan": return getMatchingId(doc[attribute]) > values[0]; case "greaterThanEqual": return getMatchingId(doc[attribute]) >= values[0]; case "search": return getMatchingId(doc[attribute]) .toLowerCase() .includes(values[0].toLowerCase()); case "isIn": return matchesValues(doc[attribute], values); case "isNotIn": return !matchesValues(doc[attribute], values); case "contains": return Array.isArray(values) ? values.some((val) => matchesValue(doc[attribute], val)) : false; case "between": const id = getMatchingId(doc[attribute]); return id !== null && id >= values[0] && id <= values[1]; case "startsWith": return String(getMatchingId(doc[attribute])).startsWith(String(values[0])); case "endsWith": return String(getMatchingId(doc[attribute])).endsWith(String(values[0])); case "isNull": return doc[attribute] === null; case "isNotNull": return doc[attribute] !== null; case "or": return values.some((subQuery) => evaluateQuery(subQuery, doc)); case "and": return values.every((subQuery) => evaluateQuery(subQuery, doc)); default: return false; } }; return evaluateQuery(parsedQuery, doc); } catch (e) { console.error("Error parsing relationship query:", query, e); return false; } }); }); } return { data: { total: filteredDocuments.length, documents: filteredDocuments, }, error: null, }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const listIndexes = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const listIndexesParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.queries, ]; const data = await databases.listIndexes(...listIndexesParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const updateBooleanAttribute = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? usersCollectionId; const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; const updateBooleanAttributeParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.key, newArgs.required, newArgs.xdefault, newArgs.newKey, ]; const data = await databases.updateBooleanAttribute(...updateBooleanAttributeParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const updateCollection = async ({ ...args }) => { try { const { databases } = await createAdminClient(); // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const newArgs = { ...args, databaseId: finalDatabaseId, }; const updateCollectionParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.name, newArgs.permissions, newArgs.documentSecurity, newArgs.enabled, ]; const collList = await databases.listCollections(newArgs.databaseId, [ Query.and([ Query.equal("name", newArgs.name), Query.equal("$id", newArgs.collectionId), ]), ]); if (collList.total < 1) { throw new Error(`Collection with name: '${newArgs.name}' / id:'${newArgs.collectionId}' not found`); } if (collList.total > 1) { throw new Error(`Collection with name: '${newArgs.name}' / id:'${newArgs.collectionId}' not unique, multiple collections with the same name and/or id found`); } const data = await databases.updateCollection(...updateCollectionParams); return { data, error: null }; } catch (error) { return { data: null, error: await handleApwError({ error }), }; } }; const updateCollectionWithSchema = async (args) => { // Use provided databaseId/collectionId if available; otherwise use defaults. const finalDatabaseId = args.databaseId ?? databaseId; const finalCollectionId = args.collectionId ?? ID.unique(); // Initialize a log object. const logTopic = "migration"; const logDetails = "schemaUpdate"; const logContent = { id: await generateMigrationId(args.name), executed_at: new Date().toISOString(), status: "success", databaseId: finalDatabaseId, collectionId: finalCollectionId, changes: [], }; try { const { databases } = await createAdminClient(); const newArgs = { ...args, databaseId: finalDatabaseId, collectionId: finalCollectionId, }; // List collections. logContent.changes.push({ action: "listCollections", information: `Listing collection '${newArgs.name}' (id: '${newArgs.collectionId}') in database '${newArgs.databaseId}'.`, }); const collList = await databases.listCollections(newArgs.databaseId, [ Query.and([ Query.equal("name", newArgs.name), Query.equal("$id", newArgs.collectionId), ]), ]); if (collList.total < 1) { logContent.changes.push({ action: "listCollections", information: `Collection '${newArgs.name}' (id: '${newArgs.collectionId}') not found.`, }); throw new Error(`Collection '${newArgs.name}' (id: '${newArgs.collectionId}') not found`); } if (collList.total > 1) { logContent.changes.push({ action: "listCollections", information: `Collection '${newArgs.name}' (id: '${newArgs.collectionId}') not unique.`, }); throw new Error(`Collection '${newArgs.name}' (id: '${newArgs.collectionId}') not unique`); } logContent.changes.push({ action: "listCollections", information: `Found collection '${newArgs.name}' (id: '${newArgs.collectionId}').`, }); const currentCollection = collList.collections[0]; // Retrieve the schema. const schema = await getSchema(newArgs.name, logContent); if (!schema || !isCollectionSchema(schema)) { logContent.changes.push({ action: "getSchema", information: `No valid schema found for collection '${newArgs.name}'.`, }); throw new Error(`No schema found for collection '${newArgs.name}'`); } // Now schema is of type CollectionSchema if (!schema.attributes || schema.attributes.length < 1) { logContent.changes.push({ action: "getSchema", information: `No attributes found in schema '${schema.collectionName}'.`, }); throw new Error(`No attributes found in schema for collection '${newArgs.name}'`); } logContent.changes.push({ action: "getSchema", information: `Schema '${schema.collectionName}' loaded.`, }); // Update newArgs with schema values. newArgs.name = schema.collectionName; newArgs.permissions = schema.permissions; newArgs.documentSecurity = schema.documentSecurity; newArgs.enabled = schema.enabled; // Compare collection keys: permissions, documentSecurity, enabled. let coll; if (JSON.stringify(newArgs.permissions) !== JSON.stringify(currentCollection.$permissions) || newArgs.documentSecurity !== currentCollection.documentSecurity || newArgs.enabled !== currentCollection.enabled) { const updateCollectionParams = [ newArgs.databaseId, newArgs.collectionId, newArgs.name, newArgs.permissions, newArgs.documentSecurity, newArgs.enabled, ]; coll = await databases.updateCollection(...updateCollectionParams); logContent.changes.push({ action: "updateCollection", information: `Collection updated with new schema values.`, }); } else { coll = currentCollection; logContent.changes.push({ action: "updateCollection", information: `No update necessary for permissions, documentSecurity, or enabled.`, }); } // Process attributes. const currentAttributeKeys = new Set(coll.attributes.map((attr) => attr.key)); const schemaAttributeKeys = new Set(schema.attributes.map((attr) => attr.key)); for (const schemaAttr of schema.attributes) { if (!currentAttributeKeys.has(schemaAttr.key)) { logContent.changes.push({ action: "createAttribute", information: `Attribute '${schemaAttr.key}' not found; creating it.`, }); // Validate that xdefault is not defined if required is set to true const hasRequiredTrue = "required" in schemaAttr && schemaAttr.required === true; const hasXdefault = schemaAttr.type !== "relationship" && "xdefault" in schemaAttr && schemaAttr.xdefault !== undefined; if (hasRequiredTrue && hasXdefault) { const msg = `Not able to create Attribute '${schemaAttr.key}' for Collection '${newArgs.name}' (id: '${newArgs.collectionId}'), because both 'xdefault' and 'required: true' are set. Appwrite does not allow xdefault to be defined while required is true.`; logContent.changes.push({ action: "createAttribute", information: msg, }); throw new Error(msg); } // Check relationshipAttributeData prop if (schemaAttr.key === "relationship") { if (newArgs.relationshipAttributeData?.length) { newArgs.relationshipAttributeData.map((relAttr) => { if (!relAttr.relationshipKey.length || !relAttr.relatedCollectionId.length) { logContent.changes.push({ action: "createAttribute (relationship)", information: `Not able to create Attribute '${newArgs.name}' (id: '${newArgs.collectionId}'), since relationship information for key '${schemaAttr.key}' is missing (relationshipKey or relatedCollectionId or both, passed with param 'relationshipAttributeData' are are not valid)`, }); throw new Error(`Not able to create Attribute '${newArgs.name}' (id: '${newArgs.collectionId}'), since relationship information for key '${schemaAttr.key}' is missing (relationshipKey or relatedCollectionId or both, passed with param 'relationshipAttributeData' are are not valid)`); } }); } else { logContent.changes.push({ action: "createAttribute (relationship)", information: `Not able to create Attribute '${newArgs.name}' (id: '${newArgs.collectionId}'), since relationship information for key '${schemaAttr.key}' is missing (relationshipAttributeData)`, }); throw new Error(`Not able to create Attribute '${newArgs.name}' (id: '${newArgs.collectionId}'), since relationship information for key '${schemaAttr.key}' is missing (relationshipAttributeData)`); } } try { await createAttribute(newArgs.databaseId, newArgs.collectionId, schemaAttr, newArgs.relationshipAttributeData?.find((relAttr) => relAttr.relationshipKey === schemaAttr.key)?.relatedCollectionId); logContent.changes.push({ action: "createAttribute", information: `Attribute '${schemaAttr.key}' created.`, }); } catch (error) { throw new Error(`Couldn't create attribute '${schemaAttr}': ${error}`); } } else if (args.destructive) { const existingAttr = coll.attributes.find((attr) => attr.key === schemaAttr.key); if (!attributesEqual(existingAttr, schemaAttr)) { logContent.changes.push({ action: "updateAttribute", infor