/* eslint-disable max-len */ import { z } from 'zod'; // ============================================================================ export const SupportedScopeSpecSchema = z.enum(['none', 'single', 'list']); export type SupportedScopeSpec = z.infer; // ============================================================================ export const DateFormatSpecSchema = z.enum(['ISO', 'Unix']); export type DateSpecFormat = z.infer; // ============================================================================ export const HttpMethodSpecSchema = z.enum(['get', 'post']); export type HttpMethodSpec = z.infer; // ============================================================================ export const StatusResultSpecSchema = z.enum(['error', 'warning', 'throttled']); export type StatusResultSpec = z.infer; // ============================================================================ export const HeadersSpecSchema = z.record(z.any()); export type HeadersSpec = z.infer; // ============================================================================ export const QueryArgsSpecSchema = z.record(z.any()); export type QueryArgsSpec = z.infer; // ============================================================================ export const ApiInParameterSpecSchema = z.object({ mode: z.enum(['queryArg', 'header']), path: z.string(), suggestedValue: z.union([z.string(), z.number(), z.boolean()]).optional() }).strict(); export type ApiInParameterSpec = z.infer; // ============================================================================ export const ApiOutParameterSpecSchema = z.object({ mode: z.enum(['webLink', 'header', 'payload']), path: z.string() }).strict(); export type ApiOutParameterSpec = z.infer; // ============================================================================ export const ApiOutCalculatedParameterSpecSchema = ApiOutParameterSpecSchema.merge(z.object({ expression: z.string().optional() })); export type ApiOutCalculatedParameterSpec = z.infer; // ============================================================================ export const AuthSpecSchema = z.object({ type: z.enum(['bearerToken', 'basicUserPassword', 'oauth2']), token: z.string().optional(), username: z.string().optional(), password: z.string().optional(), sendTokenInParameters: z.boolean().optional(), oauth2GrantType: z.enum(['clientCredentials', 'password', 'authCode']).optional(), oauth2TokenUrl: z.string().optional(), oauth2Scope: z.string().optional(), oauth2ClientSecretLocationDuringAuth: z.enum(['query', 'header', 'body']).optional(), oauth2ClientId: z.string().optional(), oauth2ClientSecret: z.string().optional() }).strict().merge(ApiInParameterSpecSchema); export type AuthSpec = z.infer; // ============================================================================ export const PagingModelSpecSchema = z.enum(['offset', 'nextUrl', 'token']); export type PagingModelSpec = z.infer; // ============================================================================ export const PagingOptionsSpecSchema = z.object({ baseUrlTemplate: z.string().optional() }); export type PagingOptionsSpec = z.infer; // ============================================================================ export const TimeframeSpecSchema = z.object({ format: DateFormatSpecSchema, summarizeColumns: z.array(z.string()).optional() }).strict(); export type TimeframeSpec = z.infer; // ============================================================================ export const StatusSpecSchema = z.object({ name: z.string(), condition: z.string(), result: StatusResultSpecSchema, message: z.string().optional() }).strict(); export type StatusSpec = z.infer; // ============================================================================ export const ScopeSpecSchema = z.object({ /** * When supportedScope is 'list', mustacheData defines how the replacements * data object "scope" is populated for use in mustache substitutions. */ idPath: z.string().optional(), // default = "sourceId" idSeparator: z.string().optional() // default = "," }).strict(); export type ScopeSpec = z.infer; // ============================================================================ export const BodyTypeSchema = z.enum(['json', 'x-www-form-urlencoded']); export type BodyType = z.infer; // ============================================================================ export const BaseApiSpecSchema = z.object({ url: z.string(), httpMethod: HttpMethodSpecSchema, headers: HeadersSpecSchema.optional(), queryArgs: QueryArgsSpecSchema.optional(), body: z.object({}).optional(), bodies: z.array(z.object({ condition: z.string(), body: z.object({}), break: z.boolean().optional() })).optional(), bodyType: BodyTypeSchema.optional(), auth: AuthSpecSchema.optional(), xmlPayload: z.boolean().optional(), dataPath: z.string().optional(), isScalar: z.boolean().optional(), pagingModel: PagingModelSpecSchema.optional(), pagingOptions: PagingOptionsSpecSchema.optional(), pageSizeIn: ApiInParameterSpecSchema.optional(), nextUrlOut: ApiOutParameterSpecSchema.optional(), nextTokenIn: ApiInParameterSpecSchema.optional(), nextTokenOut: ApiOutParameterSpecSchema.optional(), nextOffsetIn: ApiInParameterSpecSchema.optional(), retryAfter: ApiOutCalculatedParameterSpecSchema.optional(), status: z.array(StatusSpecSchema).optional() }).strict(); export type BaseApiSpec = z.infer; export const ApiDefinitionSpecSchema = BaseApiSpecSchema.merge(z.object({ name: z.string() })); export type ApiDefinitionSpec = z.infer; export const ApiSpecSchema = BaseApiSpecSchema.partial().merge(z.object({ extends: z.string().optional() })); export type ApiSpec = z.infer; // ============================================================================ export const LookUpResultSpecSchema = z.object({ path: z.string(), groupBy: z.string().optional() }); export type LookUpResultSpec = z.infer; // ============================================================================ export const LookUpSpecSchema = z.object({ name: z.string(), api: ApiSpecSchema.optional(), result: LookUpResultSpecSchema }); export type LookUpSpec = z.infer; // ============================================================================ export const LookUpRefArgSpecSchema = z.object({ name: z.string(), arg: z.any() }).strict(); export type LookUpRefArgSpec = z.infer; // ============================================================================ export const LookUpRefSpecSchema = z.object({ condition: z.string().optional(), name: z.string(), lookUp: z.string(), args: z.array(LookUpRefArgSpecSchema).optional() }).strict(); export type LookUpRefSpec = z.infer; // ============================================================================ export const CalculationSpecSchema = z.object({ condition: z.string().optional(), name: z.string(), expression: z.string() }).strict(); export type CalculationSpec = z.infer; // ============================================================================ export const StreamDataColumnRoleSchema = z.union([ z.literal('label'), z.literal('value'), z.literal('timestamp'), z.literal('unitLabel'), z.literal('id'), z.literal('sourceId'), z.literal('link'), z.literal('none') ]); export type StreamDataColumnRole = z.infer; // ============================================================================ export const DataTransformSpecSchema = z.object({ name: z.string(), flags: z.boolean().optional(), map: z.array(z.object({ from: z.any(), to: z.any() })), default: z.any().optional() }).strict(); export type DataTransformSpec = z.infer; // ============================================================================ export const FieldSpecSchema = z.object({ /** * The name of the property in the created vertex or row to hold the value from the payload */ name: z.string(), /** * A condition to evaluate to decide whether to include this field. Conditions are expressed * as strings contain a JavaScript expression. Mustache-substitution is run on this string before * it is parsed by the npm package "esprima-next" and then evaluated using the npm package * "eval-estree-expression". */ condition: z.string().optional(), /** * true if the value should be converted to a string */ convertToString: z.boolean().optional(), /** * true if the value should be converted to a Unix date (unix dates lose precision... * perhaps we should switch to ISO date strings?) */ convertToDate: z.boolean().optional(), /** * (if converting to a date) the format of the date in the payload received */ dateFormat: DateFormatSpecSchema.optional(), /** * Steps (calculations or lookups) to be performed for this field */ calc: z.array(z.union([CalculationSpecSchema, LookUpRefSpecSchema])).optional(), /* * If the fields value should be transformed by a DataTransform, its name is specified here */ dataTransform: z.string().optional(), /** * A string which identifies whence in the payload (or the results of a lookup), the * value is obtained (supports mustache or a special "@" prefix to give a simple path * to the data in the payload) */ value: z.string(), /** * (for data stream rows only) indicates that the field is a timestamp field */ isTimestamp: z.boolean().optional(), /** * (for data stream rows only) set this to true to suppress subsequent rows with the * a value for this field that was already encountered. */ distinct: z.boolean().optional(), /** * (for data stream rows only) for a field which is the sourceId of one of the targetNode, * entering a field name here will cause an extra field to be generated (being the original * id value of the targetNode). */ idForSourceId: z.string().optional(), /** * (for data stream rows only) supplies column metadata for platform */ displayName: z.string().optional(), visible: z.boolean().optional(), include: z.boolean().optional(), shape: z.union([z.string().optional(), z.tuple([z.string(), z.record(z.any())]), z.tuple([z.string()])]).optional(), role: StreamDataColumnRoleSchema.optional() }).strict(); export type FieldSpec = z.infer; // ============================================================================ export const LinkSpecSchema = z.object({ label: z.string(), url: z.string() }).strict(); export type LinkSpec = z.infer; // ============================================================================ export const EdgeSpecSchema = z.object({ label: z.string(), inV: z.string(), outV: z.string(), forEach: z.string().optional() }).strict(); export type EdgeSpec = z.infer; // ============================================================================ export const BaseTypeImportSpecSchema = z.object({ type: z.string(), sourceType: z.string(), api: ApiSpecSchema.optional(), fields: z.array(FieldSpecSchema), links: z.array(LinkSpecSchema).optional(), edges: z.array(EdgeSpecSchema).optional() }).strict(); export type BaseTypeImportSpec = z.infer; export const TypeImportForEachSpecSchema = z.object({ forEach: z.string().optional() }).strict(); export type TypeImportForEachSpec = z.infer; export const BaseDependentTypeImportSpecSchema = BaseTypeImportSpecSchema.merge(TypeImportForEachSpecSchema); export type BaseDependentTypeImportSpec = z.infer; export type TypeImportSpec = z.infer & { dependentObjects?: BaseDependentTypeImportSpec[]; }; export const TypeImportSpecSchema: z.ZodType = BaseTypeImportSpecSchema.extend({ dependentObjects: z.lazy(() => BaseDependentTypeImportSpecSchema.array().optional()) }); export type DependentTypeImportSpec = z.infer & { dependentObjects?: BaseDependentTypeImportSpec[]; }; export const DependentTypeImportSpecSchema: z.ZodType = BaseDependentTypeImportSpecSchema.extend({ dependentObjects: z.lazy(() => BaseDependentTypeImportSpecSchema.array().optional()) }); // ============================================================================ export const TypeImportsSpecSchema = z.object({ types: z.array(TypeImportSpecSchema) }).strict(); export type TypeImportsSpec = z.infer; // ============================================================================ export const DataSourceSpecSchema = z.object({ /** * Name of the data source (must match that in data_streams.json) */ name: z.string(), /** * Scope type supported (none, single or list) */ supportedScope: SupportedScopeSpecSchema, /** * Timeframe info */ timeframe: TimeframeSpecSchema.optional(), /** * Scope info */ scope: ScopeSpecSchema.optional(), /** * Steps (calculations or lookups) to be performed before the main API call */ preCalc: z.array(z.union([CalculationSpecSchema, LookUpRefSpecSchema])).optional(), /** * The API to call to get the resulting data */ api: ApiSpecSchema.optional(), /** * How to format the fields in each row of output */ fields: z.array(FieldSpecSchema), /** * Conditional filtering of result rows (after fields have been extracted) */ filterResults: z.object({ condition: z.string() }).optional() }).strict(); export type DataSourceSpec = z.infer; // ============================================================================ export const TestConfigSpecSchema = z.object({ apis: z.array(ApiSpecSchema), helpUrl: z.string().optional() }).strict(); export type TestConfigSpec = z.infer; // ============================================================================ /** * The top-level webapi schema */ export const WebApiSpecSchema = z.object({ webApiVersion: z.string().regex(/^\d+\.\d+\.\d+(\.\d+)?$/u), api: z.array(ApiDefinitionSpecSchema).optional(), lookUps: z.array(LookUpSpecSchema).optional(), dataTransforms: z.array(DataTransformSpecSchema).optional(), testConfig: TestConfigSpecSchema.optional(), import: TypeImportsSpecSchema, dataSources: z.array(DataSourceSpecSchema), $schema: z.string().optional() // This allows users to hook the schema up in VSC without getting a warning }).strict(); export type WebApiSpec = z.infer; /** * Function to confirm if object is a valid Web API schema * * @param obj * @returns true if valid or throws the zod exception if not */ export function CheckWebApiSpec(obj: unknown): boolean { WebApiSpecSchema.parse(obj); return true; }