@embeddable.com/sdk-core
Version:
Core Embeddable SDK module responsible for web-components bundling and publishing.
263 lines (220 loc) • 6.62 kB
text/typescript
import * as fs from "node:fs/promises";
import * as YAML from "yaml";
import { errorFormatter, findFiles } from "@embeddable.com/sdk-utils";
import { z } from "zod";
import ora from "ora";
import { checkNodeVersion } from "./utils";
import { ResolvedEmbeddableConfig } from "./defineConfig";
const CUBE_YAML_FILE_REGEX = /^(.*)\.cube\.ya?ml$/;
const SECURITY_CONTEXT_FILE_REGEX = /^(.*)\.sc\.ya?ml$/;
const CLIENT_CONTEXT_FILE_REGEX = /^(.*)\.cc\.ya?ml$/;
export default async (ctx: ResolvedEmbeddableConfig) => {
checkNodeVersion();
const spinnerValidate = ora("Data model validation...").start();
const cubeFilesList = await findFiles(
ctx.client.modelsSrc || ctx.client.srcDir,
CUBE_YAML_FILE_REGEX,
);
const securityContextFilesList = await findFiles(
ctx.client.presetsSrc || ctx.client.srcDir,
SECURITY_CONTEXT_FILE_REGEX,
);
const clientContextFilesList = await findFiles(
ctx.client.presetsSrc || ctx.client.srcDir,
CLIENT_CONTEXT_FILE_REGEX,
);
const dataModelErrors = await dataModelsValidation(cubeFilesList);
if (dataModelErrors.length) {
spinnerValidate.fail("One or more cube.yaml files are invalid:");
dataModelErrors.forEach((errorMessage) =>
spinnerValidate.info(errorMessage),
);
process.exit(1);
}
spinnerValidate.succeed("Data model validation completed");
const securityContextErrors = await securityContextValidation(
securityContextFilesList,
);
const clientContextErrors = await clientContextValidation(
clientContextFilesList,
);
if (securityContextErrors.length) {
spinnerValidate.fail("One or more security context files are invalid:");
securityContextErrors.forEach((errorMessage) =>
spinnerValidate.info(errorMessage),
);
process.exit(1);
}
if (clientContextErrors.length) {
spinnerValidate.fail("One or more client context files are invalid:");
clientContextErrors.forEach((errorMessage) =>
spinnerValidate.info(errorMessage),
);
process.exit(1);
}
return (
dataModelErrors.length === 0 &&
securityContextErrors.length === 0 &&
clientContextErrors.length === 0
);
};
export async function dataModelsValidation(filesList: [string, string][]) {
const errors: string[] = [];
for (const [_, filePath] of filesList) {
const fileContentRaw = await fs.readFile(filePath, "utf8");
try {
const cube = YAML.parse(fileContentRaw);
if (!cube?.cubes && !cube?.views) {
return [`${filePath}: At least one cubes or views must be defined`];
}
const cubeModelSafeParse = cubeModelSchema.safeParse(cube);
const viewModelSafeParse = viewModelSchema.safeParse(cube);
if (cube.cubes && !cubeModelSafeParse.success) {
errorFormatter(cubeModelSafeParse.error.issues).forEach((error) => {
errors.push(`${filePath}: ${error}`);
});
}
if (cube.views && !viewModelSafeParse.success) {
errorFormatter(viewModelSafeParse.error.issues).forEach((error) => {
errors.push(`${filePath}: ${error}`);
});
}
} catch (e: any) {
errors.push(`${filePath}: ${e.message}`);
}
}
return errors;
}
export async function securityContextValidation(filesList: [string, string][]) {
const errors: string[] = [];
const nameSet = new Set<string>();
for (const [_, filePath] of filesList) {
const fileContentRaw = await fs.readFile(filePath, "utf8");
const cube = YAML.parse(fileContentRaw);
cube.forEach((item: { name: string }) => {
if (nameSet.has(item.name)) {
errors.push(
`${filePath}: security context with name "${item.name}" already exists`,
);
} else {
nameSet.add(item.name);
}
});
const safeParse = securityContextSchema.safeParse(cube);
if (!safeParse.success) {
errorFormatter(safeParse.error.issues).forEach((error) => {
errors.push(`${filePath}: ${error}`);
});
}
}
return errors;
}
export async function clientContextValidation(filesList: [string, string][]) {
const errors: string[] = [];
const nameSet = new Set<string>();
for (const [_, filePath] of filesList) {
const fileContentRaw = await fs.readFile(filePath, "utf8");
const cube = YAML.parse(fileContentRaw);
cube.forEach((item: { name: string }) => {
if (nameSet.has(item.name)) {
errors.push(
`${filePath}: client context with name "${item.name}" already exists`,
);
} else {
nameSet.add(item.name);
}
});
const safeParse = clientContextSchema.safeParse(cube);
if (!safeParse.success) {
errorFormatter(safeParse.error.issues).forEach((error) => {
errors.push(`${filePath}: ${error}`);
});
}
}
return errors;
}
enum MeasureTypeEnum {
string = "string",
time = "time",
boolean = "boolean",
number = "number",
count = "count",
count_distinct = "count_distinct",
count_distinct_approx = "count_distinct_approx",
sum = "sum",
avg = "avg",
min = "min",
max = "max",
}
enum DimensionTypeEnum {
string = "string",
time = "time",
boolean = "boolean",
number = "number",
geo = "geo",
}
const cubeModelSchema = z
.object({
cubes: z
.object({
name: z.string(),
dimensions: z
.object({
name: z.string(),
type: z.nativeEnum(DimensionTypeEnum),
})
.array()
.optional(),
measures: z
.object({
name: z.string(),
type: z.nativeEnum(MeasureTypeEnum),
})
.array()
.optional(),
})
.array()
.min(1),
})
.refine(
(data) =>
data.cubes.every(
(cube) => cube.dimensions?.length || cube.measures?.length,
),
{
message: "At least one measure or dimension must be defined",
path: ["cubes"],
},
);
const viewModelSchema = z.object({
views: z
.object({
name: z.string(),
cubes: z
.object({
join_path: z.string(),
})
.array(),
})
.array()
.min(1),
});
const securityContextSchema = z.array(
z
.object({
name: z.string(),
securityContext: z.object({}), // can be any object
environment: z.string().optional(),
useQueryRewrite: z.boolean().optional(),
})
.strict(),
);
const clientContextSchema = z.array(
z
.object({
name: z.string(),
clientContext: z.object({}), // can be any object
canvas: z.object({}).optional(),
})
.strict(),
);