mirror of
https://github.com/marcogll/passkit-generator.git
synced 2026-03-15 18:25:24 +00:00
Improved schemas validation and added new function assertValidity
This commit is contained in:
104
src/PKPass.ts
104
src/PKPass.ts
@@ -87,18 +87,14 @@ export default class PKPass extends Bundle {
|
|||||||
|
|
||||||
buffers = await getModelFolderContents(source.model);
|
buffers = await getModelFolderContents(source.model);
|
||||||
certificates = source.certificates;
|
certificates = source.certificates;
|
||||||
props = source.props ?? {};
|
props = Schemas.validate(Schemas.OverridablePassProps, props);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (additionalProps && Object.keys(additionalProps).length) {
|
if (additionalProps && Object.keys(additionalProps).length) {
|
||||||
const validation = Schemas.getValidated(
|
Object.assign(
|
||||||
additionalProps,
|
props,
|
||||||
Schemas.OverridablePassProps,
|
Schemas.validate(Schemas.OverridablePassProps, additionalProps),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (validation) {
|
|
||||||
Object.assign(props, validation);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PKPass(buffers, certificates, props);
|
return new PKPass(buffers, certificates, props);
|
||||||
@@ -161,9 +157,9 @@ export default class PKPass extends Bundle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Overrides validation and pushing in props */
|
/** Overrides validation and pushing in props */
|
||||||
const overridesValidation = Schemas.getValidated(
|
const overridesValidation = Schemas.validate(
|
||||||
props,
|
|
||||||
Schemas.OverridablePassProps,
|
Schemas.OverridablePassProps,
|
||||||
|
props,
|
||||||
);
|
);
|
||||||
|
|
||||||
Object.assign(this[propsSymbol], overridesValidation);
|
Object.assign(this[propsSymbol], overridesValidation);
|
||||||
@@ -180,10 +176,7 @@ export default class PKPass extends Bundle {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
public set certificates(certs: Schemas.CertificatesSchema) {
|
public set certificates(certs: Schemas.CertificatesSchema) {
|
||||||
if (!Schemas.isValid(certs, Schemas.CertificatesSchema)) {
|
Schemas.assertValidity(Schemas.CertificatesSchema, certs);
|
||||||
throw new TypeError("Cannot set certificates: invalid");
|
|
||||||
}
|
|
||||||
|
|
||||||
this[certificatesSymbol] = certs;
|
this[certificatesSymbol] = certs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,17 +204,7 @@ export default class PKPass extends Bundle {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
Schemas.assertValidity(Schemas.TransitType, value);
|
||||||
* @TODO Make getValidated more explicit in case of error.
|
|
||||||
* @TODO maybe make an automated error.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!Schemas.getValidated(value, Schemas.TransitType)) {
|
|
||||||
throw new TypeError(
|
|
||||||
`Cannot set transitType to '${value}': invalid type. Expected one of PKTransitTypeAir, PKTransitTypeBoat, PKTransitTypeBus, PKTransitTypeGeneric, PKTransitTypeTrain.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this[propsSymbol]["boardingPass"].transitType = value;
|
this[propsSymbol]["boardingPass"].transitType = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,11 +287,7 @@ export default class PKPass extends Bundle {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
public set type(type: Schemas.PassTypesProps) {
|
public set type(type: Schemas.PassTypesProps) {
|
||||||
if (!Schemas.isValid(type, Schemas.PassType)) {
|
Schemas.assertValidity(Schemas.PassType, type);
|
||||||
throw new TypeError(
|
|
||||||
`Invalid type. Expected one of 'boardingPass' | 'coupon' | 'storeCard' | 'eventTicket' | 'generic' but received '${type}'`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.type) {
|
if (this.type) {
|
||||||
/**
|
/**
|
||||||
@@ -398,12 +377,10 @@ export default class PKPass extends Bundle {
|
|||||||
const prsJSON = JSON.parse(
|
const prsJSON = JSON.parse(
|
||||||
buffer.toString(),
|
buffer.toString(),
|
||||||
) as Schemas.Personalization;
|
) as Schemas.Personalization;
|
||||||
const personalizationValidation = Schemas.getValidated(
|
|
||||||
prsJSON,
|
|
||||||
Schemas.Personalization,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!personalizationValidation) {
|
try {
|
||||||
|
Schemas.assertValidity(Schemas.Personalization, prsJSON);
|
||||||
|
} catch (err) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Personalization.json file has been omitted as invalid.",
|
"Personalization.json file has been omitted as invalid.",
|
||||||
);
|
);
|
||||||
@@ -458,27 +435,13 @@ export default class PKPass extends Bundle {
|
|||||||
...otherPassData
|
...otherPassData
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
/**
|
|
||||||
* Validating the rest of the data and
|
|
||||||
* importing all the props. They are going
|
|
||||||
* to overwrite props setted by user but
|
|
||||||
* we can't do much about.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const validation = Schemas.getValidated(
|
|
||||||
otherPassData,
|
|
||||||
Schemas.PassProps,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (validation) {
|
|
||||||
if (Object.keys(this[propsSymbol]).length) {
|
if (Object.keys(this[propsSymbol]).length) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"The imported pass.json's properties will be joined with the current setted props. You might lose some data.",
|
"The imported pass.json's properties will be joined with the current setted props. You might lose some data.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(this[propsSymbol], validation);
|
Object.assign(this[propsSymbol], otherPassData);
|
||||||
}
|
|
||||||
|
|
||||||
if (!type) {
|
if (!type) {
|
||||||
if (!this[passTypeSymbol]) {
|
if (!this[passTypeSymbol]) {
|
||||||
@@ -748,8 +711,8 @@ export default class PKPass extends Bundle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this[propsSymbol]["beacons"] = Schemas.filterValid(
|
this[propsSymbol]["beacons"] = Schemas.filterValid(
|
||||||
beacons,
|
|
||||||
Schemas.Beacon,
|
Schemas.Beacon,
|
||||||
|
beacons,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,8 +745,8 @@ export default class PKPass extends Bundle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this[propsSymbol]["locations"] = Schemas.filterValid(
|
this[propsSymbol]["locations"] = Schemas.filterValid(
|
||||||
locations,
|
|
||||||
Schemas.Location,
|
Schemas.Location,
|
||||||
|
locations,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,15 +811,15 @@ export default class PKPass extends Bundle {
|
|||||||
];
|
];
|
||||||
|
|
||||||
finalBarcodes = supportedFormats.map((format) =>
|
finalBarcodes = supportedFormats.map((format) =>
|
||||||
Schemas.getValidated(
|
Schemas.validate(Schemas.Barcode, {
|
||||||
{ format, message: barcodes[0] } as Schemas.Barcode,
|
format,
|
||||||
Schemas.Barcode,
|
message: barcodes[0],
|
||||||
),
|
} as Schemas.Barcode),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
finalBarcodes = Schemas.filterValid(
|
finalBarcodes = Schemas.filterValid(
|
||||||
barcodes as Schemas.Barcode[],
|
|
||||||
Schemas.Barcode,
|
Schemas.Barcode,
|
||||||
|
barcodes as Schemas.Barcode[],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!finalBarcodes.length) {
|
if (!finalBarcodes.length) {
|
||||||
@@ -887,7 +850,7 @@ export default class PKPass extends Bundle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this[propsSymbol]["nfc"] =
|
this[propsSymbol]["nfc"] =
|
||||||
Schemas.getValidated(nfc, Schemas.NFC) ?? undefined;
|
Schemas.validate(Schemas.NFC, nfc) ?? undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,28 +880,15 @@ function freezeRecusive(object: Object) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readPassMetadata(buffer: Buffer) {
|
function readPassMetadata(buffer: Buffer) {
|
||||||
|
let contentAsJSON: Schemas.PassProps;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contentAsJSON = JSON.parse(
|
contentAsJSON = JSON.parse(
|
||||||
buffer.toString("utf8"),
|
buffer.toString("utf8"),
|
||||||
) as Schemas.PassProps;
|
) as Schemas.PassProps;
|
||||||
|
|
||||||
const validation = Schemas.getValidated(
|
|
||||||
contentAsJSON,
|
|
||||||
Schemas.PassProps,
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @TODO validation.error?
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!validation) {
|
|
||||||
throw new Error(
|
|
||||||
"Cannot validate pass.json file. Not conformant to",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return validation;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
throw new TypeError("Cannot validat Pass.json: invalid JSON");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Schemas.validate(Schemas.PassProps, contentAsJSON);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,23 +26,22 @@ export default class FieldsArray extends Array {
|
|||||||
push(...fieldsData: Schemas.Field[]): number {
|
push(...fieldsData: Schemas.Field[]): number {
|
||||||
const validFields = fieldsData.reduce(
|
const validFields = fieldsData.reduce(
|
||||||
(acc: Schemas.Field[], current: Schemas.Field) => {
|
(acc: Schemas.Field[], current: Schemas.Field) => {
|
||||||
if (
|
try {
|
||||||
!(typeof current === "object") ||
|
Schemas.assertValidity(Schemas.Field, current);
|
||||||
!Schemas.isValid(current, Schemas.Field)
|
} catch (err) {
|
||||||
) {
|
console.warn(`Cannot add field: ${err}`);
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this[poolSymbol].has(current.key)) {
|
if (this[poolSymbol].has(current.key)) {
|
||||||
fieldsDebug(
|
console.warn(
|
||||||
`Field with key "${current.key}" discarded: fields must be unique in pass scope.`,
|
`Cannot add field with key '${current.key}': another field already owns this key. Ignored.`,
|
||||||
);
|
);
|
||||||
} else {
|
return acc;
|
||||||
this[poolSymbol].add(current.key);
|
|
||||||
acc.push(current);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
this[poolSymbol].add(current.key);
|
||||||
|
return [...acc, current];
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export * from "./Personalize";
|
|||||||
export * from "./Certificates";
|
export * from "./Certificates";
|
||||||
|
|
||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import debug from "debug";
|
|
||||||
|
|
||||||
import { Barcode } from "./Barcodes";
|
import { Barcode } from "./Barcodes";
|
||||||
import { Location } from "./Location";
|
import { Location } from "./Location";
|
||||||
@@ -17,12 +16,9 @@ import { Beacon } from "./Beacons";
|
|||||||
import { NFC } from "./NFC";
|
import { NFC } from "./NFC";
|
||||||
import { Field } from "./PassFieldContent";
|
import { Field } from "./PassFieldContent";
|
||||||
import { PassFields, TransitType } from "./PassFields";
|
import { PassFields, TransitType } from "./PassFields";
|
||||||
import { Personalization } from "./Personalize";
|
|
||||||
import { Semantics } from "./SemanticTags";
|
import { Semantics } from "./SemanticTags";
|
||||||
import { CertificatesSchema } from "./Certificates";
|
import { CertificatesSchema } from "./Certificates";
|
||||||
|
|
||||||
const schemaDebug = debug("Schema");
|
|
||||||
|
|
||||||
export interface FileBuffers {
|
export interface FileBuffers {
|
||||||
[key: string]: Buffer;
|
[key: string]: Buffer;
|
||||||
}
|
}
|
||||||
@@ -168,19 +164,6 @@ export const Template = Joi.object<Template>({
|
|||||||
|
|
||||||
// --------- UTILITIES ---------- //
|
// --------- UTILITIES ---------- //
|
||||||
|
|
||||||
type AvailableSchemas =
|
|
||||||
| typeof Barcode
|
|
||||||
| typeof Location
|
|
||||||
| typeof Beacon
|
|
||||||
| typeof NFC
|
|
||||||
| typeof Field
|
|
||||||
| typeof PassFields
|
|
||||||
| typeof Personalization
|
|
||||||
| typeof TransitType
|
|
||||||
| typeof Template
|
|
||||||
| typeof CertificatesSchema
|
|
||||||
| typeof OverridablePassProps;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the passed options are compliant with the indicated schema
|
* Checks if the passed options are compliant with the indicated schema
|
||||||
* @param {any} opts - options to be checks
|
* @param {any} opts - options to be checks
|
||||||
@@ -188,69 +171,77 @@ type AvailableSchemas =
|
|||||||
* @returns {boolean} - result of the check
|
* @returns {boolean} - result of the check
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function isValid(opts: any, schema: AvailableSchemas): boolean {
|
export function isValid<T extends Object>(
|
||||||
if (!schema) {
|
opts: T,
|
||||||
schemaDebug(
|
schema: Joi.ObjectSchema<T>,
|
||||||
`validation failed due to missing or mispelled schema name`,
|
): boolean {
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = schema.validate(opts);
|
const validation = schema.validate(opts);
|
||||||
|
|
||||||
if (validation.error) {
|
if (validation.error) {
|
||||||
schemaDebug(
|
throw new TypeError(validation.error.message);
|
||||||
`validation failed due to error: ${validation.error.message}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return !validation.error;
|
return !validation.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the validation in verbose mode, exposing the value or an empty object
|
* Performs validation of a schema on an object.
|
||||||
* @param {object} opts - to be validated
|
* If it fails, will throw an error.
|
||||||
* @param {*} schemaName - selected schema
|
*
|
||||||
* @returns {object} the filtered value or empty object
|
* @param schema
|
||||||
|
* @param data
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function getValidated<T extends Object>(
|
export function assertValidity<T>(
|
||||||
opts: T,
|
schema: Joi.ObjectSchema<T> | Joi.StringSchema,
|
||||||
schema: AvailableSchemas,
|
data: T,
|
||||||
): T | null {
|
): void {
|
||||||
if (!schema) {
|
const validation = schema.validate(data);
|
||||||
schemaDebug(`validation failed due to missing schema`);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validation = schema.validate(opts, { stripUnknown: true });
|
|
||||||
|
|
||||||
if (validation.error) {
|
if (validation.error) {
|
||||||
schemaDebug(
|
throw new TypeError(validation.error.message);
|
||||||
`Validation failed in getValidated due to error: ${validation.error.message}`,
|
}
|
||||||
);
|
}
|
||||||
return null;
|
|
||||||
|
/**
|
||||||
|
* Performs validation and throws the error if there's one.
|
||||||
|
* Otherwise returns a (possibly patched) version of the specified
|
||||||
|
* options (it depends on the schema)
|
||||||
|
*
|
||||||
|
* @param schema
|
||||||
|
* @param options
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function validate<T extends Object>(
|
||||||
|
schema: Joi.ObjectSchema<T> | Joi.StringSchema,
|
||||||
|
options: T,
|
||||||
|
): T {
|
||||||
|
const validationResult = schema.validate(options, {
|
||||||
|
stripUnknown: true,
|
||||||
|
abortEarly: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validationResult.error) {
|
||||||
|
throw validationResult.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return validation.value;
|
return validationResult.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterValid<T extends Object>(
|
export function filterValid<T extends Object>(
|
||||||
|
schema: Joi.ObjectSchema<T>,
|
||||||
source: T[],
|
source: T[],
|
||||||
schema: AvailableSchemas,
|
|
||||||
): T[] {
|
): T[] {
|
||||||
if (!source) {
|
if (!source) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return source.reduce((acc, current) => {
|
return source.reduce((acc, current) => {
|
||||||
const validation = getValidated(current, schema);
|
try {
|
||||||
|
return [...acc, validate(schema, current)];
|
||||||
if (!validation) {
|
} catch {
|
||||||
return acc;
|
return [...acc];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...acc, validation];
|
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user