@premieroctet/next-admin
Version:
Next-Admin provides a customizable and turnkey admin dashboard for applications built with Next.js and powered by the Prisma ORM. It aims to simplify the development process by providing a turnkey admin system that can be easily integrated into your proje
336 lines (335 loc) • 14 kB
JavaScript
import external_lodash_get_default from "lodash.get";
import external_lodash_set_default from "lodash.set";
import external_zod_default from "zod";
const queryConditionsSchema = external_zod_default.union([
external_zod_default.literal("equals"),
external_zod_default.literal("not"),
external_zod_default.literal("in"),
external_zod_default.literal("notIn"),
external_zod_default.literal("lt"),
external_zod_default.literal("lte"),
external_zod_default.literal("gt"),
external_zod_default.literal("gte"),
external_zod_default.literal("contains"),
external_zod_default.literal("search"),
external_zod_default.literal("startsWith"),
external_zod_default.literal("endsWith"),
external_zod_default.literal("has")
]);
const filterSchema = external_zod_default.record(external_zod_default.string(), external_zod_default.union([
external_zod_default.record(queryConditionsSchema, external_zod_default.union([
external_zod_default.string(),
external_zod_default.number(),
external_zod_default.boolean(),
external_zod_default["null"](),
external_zod_default.array(external_zod_default.any())
])),
external_zod_default.lazy(()=>filterSchema),
external_zod_default.lazy(()=>relationshipSchema)
]));
const relationshipSchema = external_zod_default.object({
some: external_zod_default.union([
external_zod_default.lazy(()=>queryBlockSchema),
external_zod_default.lazy(()=>filterSchema)
])
});
const queryBlockSchema = external_zod_default.union([
external_zod_default.object({
AND: external_zod_default.lazy(()=>queryBlockSchema.array())
}),
external_zod_default.object({
OR: external_zod_default.lazy(()=>queryBlockSchema.array())
}),
filterSchema
]);
const validateQuery = (query)=>{
try {
const parsed = JSON.parse(query);
return queryBlockSchema.parse(parsed);
} catch (e) {
return false;
}
};
const getQueryCondition = (condition)=>{
try {
const queryCondition = queryConditionsSchema.parse(condition);
return queryCondition;
} catch {
return false;
}
};
const contentTypeFromSchemaType = (schemaProperty)=>{
const { type: schemaType, format, enum: schemaEnum } = schemaProperty;
const type = Array.isArray(schemaType) ? schemaType[0] : schemaType;
switch(type){
case "string":
if ("date-time" === format) return "datetime";
if (schemaEnum) return "enum";
return "text";
case "integer":
case "number":
return "number";
case "boolean":
return "boolean";
default:
return "text";
}
};
const isFieldNullable = (schemaType)=>{
const isArrayType = Array.isArray(schemaType);
if (isArrayType) return schemaType.includes("null");
return "null" === schemaType;
};
const isSchemaPropertyScalarArray = (definition, property)=>{
const schemaProperty = definition.properties[property];
return schemaProperty?.type === "array" && !schemaProperty.items?.$ref;
};
const setInternalPathToBlocks = (blocks, path = "")=>{
blocks.forEach((block, index)=>{
block.internalPath = `${path}[${index}]`;
if ("filter" !== block.type && block.children) setInternalPathToBlocks(block.children, `${path}[${index}].children`);
});
};
const cleanEmptyBlocks = (blocks)=>{
for(let i = 0; i < blocks.length; i++){
const block = blocks[i];
if (block) {
if ("filter" !== block.type && block.children) block.children = cleanEmptyBlocks(block.children);
}
}
const indicesToRemove = [];
for(let i = 0; i < blocks.length; i++){
const block = blocks[i];
if (!block) indicesToRemove.push(i);
}
indicesToRemove.forEach((idx)=>{
blocks.splice(idx, 1);
});
return blocks;
};
const getConditionFromValue = (value, condition)=>{
if (null === value) return "equals" === condition ? "null" : "nnull";
if ("has" === condition) return "equals";
return condition;
};
const getQueryBlockValue = (value, contentType, condition)=>{
if ("datetime" === contentType) try {
const dateString = new Date(value).toJSON();
return dateString.substring(0, dateString.length - 8);
} catch {
return value;
}
if (("in" === condition || "notIn" === condition) && Array.isArray(value)) return value.join(", ");
return value;
};
const buildUIBlocks = (blocks, { resource, schema, options, t }, fields = [], displayFields = [])=>{
if (blocks) {
const entries = Object.entries(blocks);
const uiBlocks = entries.flatMap(([key, value])=>{
if ("AND" === key || "OR" === key) return {
type: "AND" === key ? "and" : "or",
id: crypto.randomUUID(),
children: value.flatMap((block)=>buildUIBlocks(block, {
resource,
schema,
options
}, fields, displayFields))
};
{
const resourceInSchema = schema.definitions[resource];
const schemaProperty = resourceInSchema.properties[key];
const conditions = Object.entries(value);
const displayKeyFallback = options?.[resource]?.aliases?.[key] ?? key;
const displayKey = t?.(`model.${resource}.fields.${key}`, {}, displayKeyFallback) ?? displayKeyFallback;
if (schemaProperty) return conditions.flatMap(([conditionKey])=>{
const queryCondition = getQueryCondition(conditionKey);
if (queryCondition) {
const queryValue = value[conditionKey];
const contentType = contentTypeFromSchemaType(schemaProperty);
return {
type: "filter",
path: [
...fields,
key
].join("."),
condition: getConditionFromValue(queryValue, queryCondition),
value: getQueryBlockValue(queryValue, contentType, queryCondition),
id: crypto.randomUUID(),
..."enum" === contentType ? {
enum: schemaProperty.enum
} : {},
defaultValue: schemaProperty.default,
contentType: contentType,
canHaveChildren: false,
nullable: isFieldNullable(schemaProperty.type),
displayPath: [
...displayFields,
displayKey
].join(" \u2192 ")
};
}
{
let isArrayConditionKey = "some" === conditionKey;
if ("array" !== schemaProperty.type && isArrayConditionKey) if (!schemaProperty.properties?.some) return;
else isArrayConditionKey = false;
const childResourceName = (schemaProperty.__nextadmin?.relation?.$ref || schemaProperty?.anyOf?.[0]?.$ref)?.split("/")?.at(-1);
const childEntries = Object.entries(isArrayConditionKey ? value.some : value);
return childEntries.map(([childKey, childValue])=>{
if ("AND" === childKey || "OR" === childKey) return {
type: "AND" === childKey ? "and" : "or",
id: crypto.randomUUID(),
children: childValue.flatMap((block)=>buildUIBlocks(block, {
resource: childResourceName,
schema,
options
}, [
...fields,
key
], [
...displayFields,
displayKey
])).filter(Boolean)
};
return buildUIBlocks({
[childKey]: childValue
}, {
resource: childResourceName,
schema,
options
}, [
...fields,
key
], [
...displayFields,
displayKey
]);
}).flat();
}
});
return;
}
}).filter(Boolean);
setInternalPathToBlocks(uiBlocks);
return uiBlocks;
}
return [];
};
const getValueForUiBlock = (block)=>{
if ("filter" === block.type) {
if ("null" === block.condition || "nnull" === block.condition) return null;
if ("datetime" === block.contentType) return new Date(block.value).toISOString();
if ("in" === block.condition || "notIn" === block.condition) return block.value.split(",").map((val)=>{
val = val.trim();
if ("number" === block.contentType) return +val;
return val;
}).filter(Boolean);
if ("number" === block.contentType && !!block.value) return +block.value;
return block.value;
}
};
const getQueryBlockValueForUiBlock = (uiBlock)=>{
if ("filter" === uiBlock.type) {
if ("null" === uiBlock.condition) return {
equals: null
};
if ("nnull" === uiBlock.condition) return {
not: null
};
return {
[uiBlock.condition]: getValueForUiBlock(uiBlock)
};
}
return {};
};
const buildQueryBlocks = (blocks, { resource, schema }, acc = {}, path = "")=>{
blocks.forEach((block)=>{
if ("and" === block.type || "or" === block.type) {
const children = block.children;
if (children?.length) {
const blockKey = "and" === block.type ? "AND" : "OR";
const finalPath = [
path,
blockKey
].filter(Boolean).join(".");
external_lodash_set_default(acc, finalPath, []);
block.children?.forEach((child, index)=>{
buildQueryBlocks([
child
], {
resource,
schema
}, acc, `${finalPath}[${index}]`);
});
}
} else if ("filter" === block.type) {
const [basePath, ...rest] = block.path.split(".");
const resourceInSchema = schema.definitions[resource];
const schemaProperty = resourceInSchema.properties[basePath];
if (schemaProperty?.type === "array") if (isSchemaPropertyScalarArray(resourceInSchema, basePath)) external_lodash_set_default(acc, [
path,
basePath
].filter(Boolean).join("."), {
has: getValueForUiBlock(block)
});
else {
const childResource = schemaProperty.__nextadmin?.relation?.$ref?.split("/")?.at(-1);
if (!external_lodash_get_default(acc, [
path,
basePath
].filter(Boolean))) external_lodash_set_default(acc, [
path,
basePath
].filter(Boolean).join("."), {
some: {}
});
buildQueryBlocks([
{
...block,
path: rest.join(".")
}
], {
resource: childResource,
schema
}, acc, [
path,
basePath,
"some"
].filter(Boolean).join("."));
}
else if (schemaProperty && (schemaProperty?.__nextadmin?.relation?.$ref || schemaProperty?.anyOf?.[0]?.$ref)) {
const ref = schemaProperty?.__nextadmin?.relation?.$ref || schemaProperty?.anyOf?.[0]?.$ref;
const childResource = ref.split("/").at(-1);
if (!external_lodash_get_default(acc, [
path,
basePath
].filter(Boolean))) external_lodash_set_default(acc, [
path,
basePath
].filter(Boolean).join("."), {});
buildQueryBlocks([
{
...block,
path: rest.join(".")
}
], {
resource: childResource,
schema
}, acc, [
path,
basePath
].filter(Boolean).join("."));
} else external_lodash_set_default(acc, [
path,
basePath
].filter(Boolean).join("."), {
...external_lodash_get_default(acc, [
path,
basePath
].filter(Boolean)),
...getQueryBlockValueForUiBlock(block)
});
}
});
return acc;
};
export { buildQueryBlocks, buildUIBlocks, cleanEmptyBlocks, contentTypeFromSchemaType, getQueryCondition, isFieldNullable, isSchemaPropertyScalarArray, setInternalPathToBlocks, validateQuery };