UNPKG

parse-server-schema-manager

Version:

Schema-as-code utilities for managing Parse Server schemas, indexes, and class-level permissions.

554 lines (546 loc) 17.7 kB
var __create = Object.create; var __getProtoOf = Object.getPrototypeOf; var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __hasOwnProp = Object.prototype.hasOwnProperty; var __toESM = (mod, isNodeMode, target) => { target = mod != null ? __create(__getProtoOf(mod)) : {}; const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target; for (let key of __getOwnPropNames(mod)) if (!__hasOwnProp.call(to, key)) __defProp(to, key, { get: () => mod[key], enumerable: true }); return to; }; var __moduleCache = /* @__PURE__ */ new WeakMap; var __toCommonJS = (from) => { var entry = __moduleCache.get(from), desc; if (entry) return entry; entry = __defProp({}, "__esModule", { value: true }); if (from && typeof from === "object" || typeof from === "function") __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable })); __moduleCache.set(from, entry); return entry; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true, configurable: true, set: (newValue) => all[name] = () => newValue }); }; // src/index.ts var exports_src = {}; __export(exports_src, { syncSchemaWithObject: () => syncSchemaWithObject, setParseInstance: () => setParseInstance, manageSchema: () => manageSchema, getParseInstance: () => getParseInstance, getAllSchemas: () => getAllSchemas, diffingIndexes: () => diffingIndexes, diffingFields: () => diffingFields, diffingCLP: () => diffingCLP, createDBMLFile: () => createDBMLFile, checkSameObject: () => checkSameObject, checkSame: () => checkSame }); module.exports = __toCommonJS(exports_src); // src/functions/object.ts var checkSame = (a1, a2) => { if (typeof a1 !== typeof a2) return false; if (typeof a1 === "object") return checkSameObject(a1, a2); return a1 === a2; }; var checkSameObject = (obj1, obj2) => { if (typeof obj1 !== typeof obj2) return false; let key1, key2; try { key1 = Object.keys(obj1); key2 = Object.keys(obj2); } catch (e) { return obj1 === obj2; } if (key1.length !== key2.length) return false; if (!key1.every((t) => key2.includes(t))) return false; return key1.every((t) => checkSame(obj1[t], obj2[t])); }; // src/functions/parse.ts var configuredParse; var getGlobalParse = () => { return globalThis.Parse; }; var loadPeerParse = () => { try { return require("parse/node"); } catch { return; } }; var setParseInstance = (parse) => { configuredParse = parse; }; var getParseInstance = () => { const parse = configuredParse ?? getGlobalParse() ?? loadPeerParse(); if (!parse?.Schema) { throw new Error("parse-server-schema-manager could not find an active Parse SDK instance. In Parse Server Cloud Code, global.Parse should be available; otherwise call setParseInstance(Parse) before using schema functions."); } return parse; }; // src/functions/schema/sync.ts var ignoreIndexesKeys = ["_id_"]; var globalKeys = ["objectId", "updatedAt", "createdAt", "ACL"]; var saveSchema = async (schema) => { try { await schema.update(); } catch (e) { try { await schema.save(); } catch { throw e; } } }; var getFieldOptions = (field) => { let options = { type: field.type }; if ("targetClass" in field) { if (field.targetClass !== undefined) options.targetClass = field.targetClass; } if (field.defaultValue !== undefined) options.defaultValue = field.defaultValue; if (field.required !== undefined) options.required = field.required; return options; }; var syncSchemaWithObject = async (className, schemaObject, ignoreAttributes) => { if (!schemaObject) return; const Parse = getParseInstance(); let schema = new Parse.Schema(className); let available = { className, fields: {}, indexes: {}, classLevelPermissions: {} }; try { available = await schema.get(); } catch (e) { await schema.save(); available = await schema.get(); } const fields = structuredClone(schemaObject.fields); const indexes = structuredClone(schemaObject.indexes) || {}; const CLP = schemaObject.classLevelPermissions || {}; for (let ignore of ignoreIndexesKeys) delete indexes[ignore]; for (let ignore of ignoreAttributes) { delete fields[ignore]; delete available.fields[ignore]; } for (let key in fields) if (!available.fields || !available.fields[key]) schema.addField(key, fields[key].type, getFieldOptions(fields[key])); let objectFields = Object.keys(fields); for (let cKey in available.fields) if (!objectFields.includes(cKey) && !globalKeys.includes(cKey)) schema.deleteField(cKey); for (let cKey in available.fields) { if (!fields[cKey]) continue; let cache = JSON.parse(JSON.stringify(fields[cKey])); let availableCache = JSON.parse(JSON.stringify(available.fields[cKey])); if (availableCache.required === false && !cache.required) { delete cache["required"]; delete availableCache["required"]; } if (!checkSame(availableCache, cache)) { if (availableCache.type !== cache.type) throw `Can't change type of a column you got to remove it then add it.`; schema.addField(cKey, fields[cKey].type, getFieldOptions(fields[cKey])); } } schema.setCLP(CLP); await saveSchema(schema); for (let key in indexes) if (!available.indexes || !available.indexes[key]) schema.addIndex(key, indexes[key]); let indexesKeys = Object.keys(indexes); for (let cKey in available.indexes) { if (ignoreIndexesKeys.includes(cKey)) continue; if (!indexesKeys.includes(cKey)) schema.deleteIndex(cKey); else if (!checkSame(indexes[cKey], available.indexes[cKey])) schema.deleteIndex(cKey); } await schema.update(); for (let cKey in available.indexes) { if (ignoreIndexesKeys.includes(cKey)) continue; if (!indexesKeys.includes(cKey)) continue; if (!checkSame(indexes[cKey], available.indexes[cKey])) schema.addIndex(cKey, indexes[cKey]); } await schema.update(); }; // src/functions/schema/schema.ts var checkSecondProperties = ["type"]; var checkOptions = ["targetClass", "required", "defaultValue"]; var diffingFields = (obj1, obj2, schemaOptions) => { let add = {}; let remove = {}; let change = {}; for (let key in obj2) { if (schemaOptions?.ignoreAttributes?.includes(key)) continue; if (!obj1[key]) add[key] = obj2[key]; else { const obj1Field = obj1[key]; const obj2Field = obj2[key]; for (const prop of checkSecondProperties) { if (prop in obj1Field && prop in obj2Field) { if (obj1Field?.[prop] !== obj2Field?.[prop]) { change[key] = change[key] ?? []; change[key].push(`${prop}: ${obj1Field[prop]} -> ${obj2Field[prop]}`); } } } for (let pr of checkSecondProperties) { if (obj1Field?.[pr] !== obj2Field?.[pr]) { change[key] = change[key] ?? []; change[key].push(`${pr}: ${obj1Field[pr]} -> ${obj2Field[pr]}`); } } for (let pr of checkOptions) if (!checkSame(obj1Field?.[pr], obj2Field?.[pr])) { if (pr === "required" && obj1[key]?.[pr] === false && !obj2[key]?.[pr]) continue; if (pr === "defaultValue" && obj1[key]?.[pr] === false && !obj2[key]?.[pr]) continue; change[key] = change[key] ?? []; change[key].push(`${pr}: ${JSON.stringify(obj1Field[pr])} -> ${JSON.stringify(obj2Field?.[pr])}`); } } } for (let key in obj1) { if (schemaOptions?.ignoreAttributes?.includes(key)) continue; if (!obj2[key]) remove[key] = obj1[key]; } const output = {}; if (Object.keys(add).length) output.add = add; if (Object.keys(remove).length) output.remove = remove; if (Object.keys(change).length) output.change = change; return output; }; var diffingIndexes = (obj1, obj2) => { const change = {}; const add = {}; const remove = {}; for (let key in obj1) if (!obj2[key]) remove[key] = obj1[key]; for (let key in obj2) { if (!obj1?.[key]) { add[key] = obj2[key]; continue; } const allKeys = [ ...new Set([...Object.keys(obj1[key]), ...Object.keys(obj2[key])]) ]; if (allKeys.some((key2) => !checkSame(obj1[key]?.[key2], obj2[key]?.[key2]))) { change[key] = { from: obj1[key], to: obj2[key] }; } } const output = {}; if (Object.keys(add).length) output.add = add; if (Object.keys(remove).length) output.remove = remove; if (Object.keys(change).length) output.change = change; return output; }; var diffingCLP = (obj1, obj2) => { const change = {}; for (let key in obj2) { const allKeys = [...Object.keys(obj1[key])]; if (allKeys.some((key2) => !checkSame(obj1[key]?.[key2], obj2[key]?.[key2]))) { change[key] = { from: obj1[key], to: obj2[key] }; } } return change; }; var sanitizeSchemaParts = (parts) => { return Object.assign({ fields: true, indexes: true, classLevelPermissions: true }, parts); }; var sanitizeSchemaOptions = (outputOptions) => { return Object.assign({ ignoreClasses: ["_Session"], ignoreAttributes: [ "ACL", "password", "authData", "emailVerified", "email" ] }, outputOptions); }; var getAllParseSchema = async () => { const Parse = getParseInstance(); return await Parse.Schema.all(); }; var getAllSchemas = async (parts = {}, outputOptions = {}) => { const schemaParts = sanitizeSchemaParts(parts); const options = sanitizeSchemaOptions(outputOptions); const { ignoreClasses, ignoreAttributes } = options; const list = await getAllParseSchema(); const clone = structuredClone(list).filter((c) => !ignoreClasses.includes(c.className)); const returnList = []; for (let cls of clone) { let obj = { className: cls.className, fields: {} }; if (schemaParts.fields) { obj.fields = cls.fields; for (let atr of ignoreAttributes ?? []) if (obj.fields?.[atr]) delete obj.fields[atr]; } if (schemaParts.indexes) obj.indexes = cls.indexes; if (schemaParts.classLevelPermissions) obj.classLevelPermissions = cls.classLevelPermissions; returnList.push(obj); } return returnList; }; var diffSchemaChanges = (existingSchema, schema, part, schemaOptions) => { const change = {}; for (let cls of schema) { const className = cls.className; if (schemaOptions?.ignoreClasses?.includes(className)) continue; const existingCls = existingSchema.find((c) => c.className === className); if (!existingCls?.fields || !cls.fields) continue; const diff = part === "fields" ? diffingFields(existingCls.fields, cls.fields, schemaOptions) : part === "indexes" ? diffingIndexes(existingCls.indexes, cls.indexes) : diffingCLP(existingCls.classLevelPermissions, cls.classLevelPermissions); if (Object.keys(diff).length) change[className] = diff; } return change; }; var addRemoveSchemaChanges = (existingSchema, schema, schemaOptions) => { const add = {}; const remove = {}; for (let cls of schema) { const className = cls.className; if (schemaOptions?.ignoreClasses?.includes(className)) continue; const existingCls = existingSchema.find((c) => c.className === className); if (existingCls) continue; add[className] = cls; } for (let cls of existingSchema) { const className = cls.className; if (schemaOptions?.ignoreClasses?.includes(className)) continue; const newCls = schema.find((c) => c.className === className); if (newCls) continue; remove[className] = cls; } const output = {}; if (Object.keys(add).length) output.add = add; if (Object.keys(remove).length) output.remove = remove; return output; }; var manageSchema = async (schema, { commit = false, remove = false, purge = false }, actionParts = {}, schemaOptions = {}) => { const schemaParts = sanitizeSchemaParts(actionParts); const options = sanitizeSchemaOptions(schemaOptions); const existingSchema = await getAllSchemas(actionParts, options); const addRemove = addRemoveSchemaChanges(existingSchema, schema, options); const changesDiff = {}; for (let key in schemaParts) if (schemaParts[key]) changesDiff[key] = diffSchemaChanges(existingSchema, schema, key, options); let log = "Nothing changed!"; if (commit) { for (let key in addRemove.add ?? {}) await syncSchemaWithObject(key, addRemove.add?.[key], options.ignoreAttributes); let keysToSync = new Set; for (let classKey in changesDiff.fields ?? {}) keysToSync.add(classKey); for (let classKey in changesDiff.indexes ?? {}) keysToSync.add(classKey); keysToSync = [...keysToSync]; for (let key of keysToSync) { const cls = schema.find((t) => t.className === key); await syncSchemaWithObject(key, cls, options.ignoreAttributes); } log = "Schema synced!"; } if (remove) { const Parse = getParseInstance(); for (let key in addRemove.remove ?? {}) { const mySchema = new Parse.Schema(key); if (purge) await mySchema.purge(); await mySchema.delete(); log = "Schema synced!"; } } const output = { ...addRemove, log }; if (Object.keys(changesDiff).length) { let tempChanges = {}; for (let key in changesDiff) { if (Object.keys(changesDiff[key] ?? {}).length) tempChanges[key] = changesDiff[key]; } if (Object.keys(tempChanges).length) output.changes = tempChanges; } return output; }; // src/functions/DBML.ts var import_fs = __toESM(require("fs")); var colors = { red: "#d32f2f", pink: "#c2185b", purple: "#7b1fa2", deepPurple: "#512da8", indigo: "#303f9f", blue: "#1976d2", lightBlue: "#0277bd", cyan: "#00838f", teal: "#00796b", green: "#2e7d32", lightGreen: "#387002", lime: "#6c6f00", yellow: "#bc5100", amber: "#c43e00", orange: "#bb4d00", deepOrange: "#ac0800", brown: "#5d4037", grey: "#616161", blueGrey: "#455a64" }; var colorByIndex = (id) => { const colorsArray = Object.values(colors); const index = id % colorsArray.length; return colorsArray[index]; }; var createDBMLFile = async (additional = {}, schemaDBML = "_SCHEMA.dbml") => { const Parse = getParseInstance(); let DBML = []; DBML.push(`// Generated by parse-server-schema-manager`); const _SCHEMA = await Parse.Schema.all(); for (let classIndex in _SCHEMA) { const parseClass = _SCHEMA[classIndex]; const className = parseClass.className; const keys = Object.keys(parseClass.fields); const fields = keys.filter((key) => !["_id", "objectId", "updatedAt", "createdAt"].includes(key)); const color = colorByIndex(Number(classIndex)); let TABLE = `Table ${className} [headercolor: ${color}] { objectId String createdAt Date [default: \`now()\`, note: "created time"] updatedAt Date [default: \`now()\`, note: "updated time"] `; const scalarFields = []; const pointerFields = []; const relationFields = []; const indexes = []; indexes.push(` objectId [pk]`); for (let ind in parseClass.indexes) { if (ind === "_id_") continue; let indKeys = Object.keys(parseClass.indexes[ind]); let first = indKeys.length > 1 ? `(${indKeys.join(", ")})` : indKeys[0]; let second = `[name: '${ind}']`; indexes.push(` ${first} ${second}`); } const dbmlOptions = {}; for (let fieldName of fields) { let field = parseClass.fields[fieldName]; const fieldType = field.type; const messages = []; if (additional?.[className]?.[fieldName]) messages.push(additional[className][fieldName]); if (field.defaultValue !== undefined) messages.push(`default: \`${JSON.stringify(field.defaultValue)}\``); const notes = []; if (field.required) notes.push("required"); if (fieldType === "Pointer") { pointerFields.push(fieldName); messages.push(`ref: > ${field.targetClass}.objectId`); notes.push("MANY-to-ONE"); } else if (fieldType === "Relation") { relationFields.push(fieldName); messages.push(`ref: - ${field.targetClass}.objectId`); notes.push("MANY-to-MANY"); } else scalarFields.push(fieldName); if (notes.length) messages.push(`note: '${notes.join(", ")}'`); const dbmlOptionsAsString = messages.length ? `[${messages.join(", ")}]` : ""; dbmlOptions[fieldName] = dbmlOptionsAsString; } scalarFields.forEach((fieldName) => { const fieldType = parseClass.fields[fieldName].type; TABLE += ` ${fieldName} ${fieldType} ${dbmlOptions[fieldName]} `; }); pointerFields.forEach((fieldName) => { TABLE += ` ${fieldName} "Pointer" ${dbmlOptions[fieldName]} `; }); relationFields.forEach((fieldName) => { TABLE += ` ${fieldName} "Relation" ${dbmlOptions[fieldName]} `; }); TABLE += ` indexes { ${indexes.join(` `)} } `; TABLE += `}`; DBML.push(TABLE); } const dbml = DBML.join(` `); import_fs.default.writeFileSync(schemaDBML, dbml); };