UNPKG

@embeddable.com/sdk-core

Version:

Core Embeddable SDK module responsible for web-components bundling and publishing.

263 lines (220 loc) 6.62 kB
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(), );