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
JavaScript
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);
};