Skip to content

Commit

Permalink
feat: allow put, config validation
Browse files Browse the repository at this point in the history
  • Loading branch information
maxakuru committed Dec 10, 2024
1 parent 1188880 commit af4e48b
Show file tree
Hide file tree
Showing 7 changed files with 1,409 additions and 4 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ module.exports = {
globals: {
__rootdir: true,
__testdir: true,
globalThis: true,
},
};
27 changes: 24 additions & 3 deletions src/config/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@

import { errorResponse } from '../utils/http.js';
import { assertAuthorization } from '../utils/auth.js';
import { validate } from '../utils/validation.js';
import ConfigSchema from '../schemas/Config.js';

/**
* @param {Context} ctx
* @param {Request} req
* @returns {Promise<Response>}
*/
export default async function configHandler(ctx) {
export default async function configHandler(ctx, req) {
const { method } = ctx.info;
if (!['GET', 'POST'].includes(method)) {
return errorResponse(405, 'method not allowed');
Expand All @@ -33,6 +36,24 @@ export default async function configHandler(ctx) {
});
}

// TODO: validate config body, set config in kv
return errorResponse(501, 'not implemented');
let json;
try {
json = await req.json();
} catch {
return errorResponse(400, 'invalid JSON');
}

const errors = validate(json, ConfigSchema);
if (errors && errors.length) {
return errorResponse(400, 'invalid body', { errors });
}

// valid, persist it
await ctx.env.CONFIGS.put(ctx.config.siteKey, JSON.stringify(json));
return new Response(JSON.stringify(json), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
}
81 changes: 81 additions & 0 deletions src/schemas/Config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

/** @type {import("../utils/validation.js").AnySchema} */
const AttributeOverrides = {
type: 'object',
properties: {
product: {
type: 'object',
properties: {},
additionalProperties: { type: 'string' },
},
variant: {
type: 'object',
properties: {},
additionalProperties: { type: 'string' },
},
},
};

/** @type {import("../utils/validation.js").AnySchema} */
const ConfigEntry = {
type: 'object',
properties: {
apiKey: { type: 'string' },
magentoEnvironmentId: { type: 'string' },
magentoWebsiteCode: { type: 'string' },
storeCode: { type: 'string' },
coreEndpoint: { type: 'string' },
catalogEndpoint: { type: 'string' },
storeViewCode: { type: 'string' },
siteOverridesKey: { type: 'string' },
host: { type: 'string' },
helixApiKey: { type: 'string' },
offerVariantURLTemplate: { type: 'string' },
attributeOverrides: AttributeOverrides,
imageParams: {
type: 'object',
properties: {},
additionalProperties: { type: 'string' },
},
pageType: {
type: 'string',
enum: ['product'],
},
headers: {
type: 'object',
properties: {},
additionalProperties: { type: 'string' },
},
imageRoles: {
type: 'array',
items: { type: 'string' },
},
},
};

/** @type {import("../utils/validation.js").AnySchema} */
const Config = {
type: 'object',
properties: {
base: {
...ConfigEntry,
required: [
'base',
],
},
},
additionalProperties: ConfigEntry,
};

export default Config;
95 changes: 94 additions & 1 deletion src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,103 @@ import type { HTMLTemplate } from "./templates/html/HTMLTemplate.js";
import { JSONTemplate } from "./templates/json/JSONTemplate.js";

declare global {
/**
* The config for a single path pattern as stored in KV
*/
export interface RawConfigEntry {
/**
* API key for Core and Catalog
*/
apiKey?: string;

/**
* Magento env ID
*/
magentoEnvironmentId?: string;

/**
* Magento website code
*/
magentoWebsiteCode?: string;

/**
* Store code
*/
storeCode?: string;

/**
* Core Commerce endpoint
*/
coreEndpoint?: string;

/**
* Catalog Service endpoint, defaults to non-sandbox
*/
catalogEndpoint?: string;

/**
* Store view code
*/
storeViewCode?: string;

/**
* Sitekey to use for overrides filename
*/
siteOverridesKey?: string;

/**
* Host to use for absolute urls
*/
host?: string;

/**
* API key for Helix, used for preview/publish during Helix Catalog API PUTs
*/
helixApiKey?: string;

/**
* Headers to send with requests to Core and Catalog
*/
headers?: Record<string, string>;

/**
* Image roles to filter by, only include images with these roles
*/
imageRoles?: string[];

/**
* Attributes to override using a different attribute name
*/
attributeOverrides?: AttributeOverrides;

/**
* Path pattern to use for offer variant URLs in JSON-LD
*/
offerVariantURLTemplate?: string;

/**
* Additional parameters to add to image URLs as query params.
*/
imageParams?: Record<string, string>;

// required for non-base entries
pageType: 'product' | string;
}

/**
* The config as stored in KV
* Each key, other than `base`, is a path pattern
* Path patterns use `{{arg}}` to denote `arg` as a path parameter
*/
export type RawConfig = {
base: RawConfigEntry;
[key: string]: RawConfigEntry;
}

/**
* { pathPattern => Config }
*/
export type ConfigMap = Record<string, Config>;
export type ConfigMap = Record<string, RawConfig>;

export interface AttributeOverrides {
variant: {
Expand Down
117 changes: 117 additions & 0 deletions src/utils/validation.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

export type FilterPrefixedKeys<TObj, TPrefix extends string> = {
[K in keyof TObj as K extends `${TPrefix}${infer I}` ? never : K]: TObj[K]
}

export type PrefixKeys<TObj, TPrefix extends string> = {
[K in keyof TObj as K extends string ? `${TPrefix}${K}` : never]: TObj[K]
}

/**
* AJV-like schema, with a slimmed down set of features
*
* AJV is >110kb and has many features we don't need on the edge,
* since the backend APIs should be validating the data too.
*/

type InvertKey = 'not.';

type Uninvertable = 'type' | 'properties' | 'items' | 'additionalItems' | 'minItems' | 'maxItems' | 'min' | 'max' |
'required' | 'additionalProperties' | 'minProperties' | 'maxProperties' | 'minLength' | 'maxLength' | 'nullable';

type InvertConditions<
T,
TUninvertible extends keyof Omit<T, 'type'> = never
> = PrefixKeys<Omit<FilterPrefixedKeys<T, InvertKey>, Uninvertable | TUninvertible>, InvertKey> & T;

export type BuiltinType = "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";

export type SchemaType = 'number' | 'integer' | 'string' | 'boolean' | 'array' | 'object' | 'null';

type AtLeastN = [n: number, conditions: Conditions[]];

interface _BaseSchema {
type: SchemaType;
enum?: any[];
constant?: any;
nullable?: boolean;
atLeastN?: AtLeastN;
}

type BaseSchema = InvertConditions<_BaseSchema>;

interface _AnyNumberSchema extends BaseSchema {
min?: number;
max?: number;
}
type AnyNumberSchema = InvertConditions<_AnyNumberSchema>;

interface _StringSchema extends BaseSchema {
type: "string";
minLength?: number;
maxLength?: number;
pattern?: RegExp;
}
export type StringSchema = InvertConditions<_StringSchema>;

export interface BooleanSchema extends BaseSchema {
type: "boolean";
}

export interface NullSchema extends BaseSchema {
type: "null";
}

export interface IntegerSchema extends AnyNumberSchema {
type: "integer";
}

export interface NumberSchema extends AnyNumberSchema {
type: "number";
}

type PropertySchema = InvertConditions<AnySchema & { matches?: string }>;

export interface ObjectSchema extends BaseSchema {
type: "object";
properties: Record<string, PropertySchema>;
required?: string[];
/** defaults to false */
additionalProperties?: boolean | PropertySchema;
minProperties?: number;
maxProperties?: number;
}

export interface ArraySchema extends BaseSchema {
items: AnySchema | AnySchema[];
additionalItems?: boolean;
minItems?: number;
maxItems?: number;
}

export type PrimitiveSchema = IntegerSchema | IntegerSchema | NumberSchema | StringSchema | BooleanSchema | NullSchema;

export type AnySchema = PrimitiveSchema | ObjectSchema | ArraySchema;

export type Conditions<TSchema extends AnySchema = AnySchema> = Omit<TSchema, Uninvertable>;

export type UninvertedConditions<TSchema extends AnySchema = AnySchema> = FilterPrefixedKeys<Conditions<TSchema>, InvertKey>;

export interface ValidationError {
path: string;
message: string;
details?: string;
}

export declare function validate(obj: unknown, schema: AnySchema): ValidationError[] | undefined;
Loading

0 comments on commit af4e48b

Please sign in to comment.