Refactored all the schemas;

Splitted them in a folder and renamed them to have the same name of typescript type;
Added more SemanticKeys;
Removed Schema resolution via string;
This commit is contained in:
Alexander Cerutti
2021-06-17 23:34:22 +02:00
parent 87321b8dc9
commit 17de1c7d79
16 changed files with 850 additions and 771 deletions

26
src/schemas/barcode.ts Normal file
View File

@@ -0,0 +1,26 @@
import Joi from "joi";
export type BarcodeFormat =
| "PKBarcodeFormatQR"
| "PKBarcodeFormatPDF417"
| "PKBarcodeFormatAztec"
| "PKBarcodeFormatCode128";
export interface Barcode {
altText?: string;
messageEncoding?: string;
format: BarcodeFormat;
message: string;
}
export const Barcode = Joi.object<Barcode>().keys({
altText: Joi.string(),
messageEncoding: Joi.string().default("iso-8859-1"),
format: Joi.string()
.required()
.regex(
/(PKBarcodeFormatQR|PKBarcodeFormatPDF417|PKBarcodeFormatAztec|PKBarcodeFormatCode128)/,
"barcodeType",
),
message: Joi.string().required(),
});

19
src/schemas/beacon.ts Normal file
View File

@@ -0,0 +1,19 @@
import Joi from "joi";
export interface Beacon {
major?: number;
minor?: number;
relevantText?: string;
proximityUUID: string;
}
export const Beacon = Joi.object<Beacon>().keys({
major: Joi.number()
.integer()
.positive()
.max(65535)
.greater(Joi.ref("minor")),
minor: Joi.number().integer().min(0).max(65535),
proximityUUID: Joi.string().required(),
relevantText: Joi.string(),
});

70
src/schemas/field.ts Normal file
View File

@@ -0,0 +1,70 @@
import Joi from "joi";
import { Semantics } from "./semantics";
export interface Field {
attributedValue?: string | number | Date;
changeMessage?: string;
dataDetectorTypes?: string[];
label?: string;
textAlignment?: string;
key: string;
value: string | number | Date;
semantics?: Semantics;
dateStyle?: string;
ignoresTimeZone?: boolean;
isRelative?: boolean;
timeStyle?: string;
currencyCode?: string;
numberStyle?: string;
}
export const Field = Joi.object<Field>().keys({
attributedValue: Joi.alternatives(
Joi.string().allow(""),
Joi.number(),
Joi.date().iso(),
),
changeMessage: Joi.string(),
dataDetectorTypes: Joi.array().items(
Joi.string().regex(
/(PKDataDetectorTypePhoneNumber|PKDataDetectorTypeLink|PKDataDetectorTypeAddress|PKDataDetectorTypeCalendarEvent)/,
"dataDetectorType",
),
),
label: Joi.string().allow(""),
textAlignment: Joi.string().regex(
/(PKTextAlignmentLeft|PKTextAlignmentCenter|PKTextAlignmentRight|PKTextAlignmentNatural)/,
"graphic-alignment",
),
key: Joi.string().required(),
value: Joi.alternatives(
Joi.string().allow(""),
Joi.number(),
Joi.date().iso(),
).required(),
semantics: Semantics,
// date fields formatters, all optionals
dateStyle: Joi.string().regex(
/(PKDateStyleNone|PKDateStyleShort|PKDateStyleMedium|PKDateStyleLong|PKDateStyleFull)/,
"date style",
),
ignoresTimeZone: Joi.boolean(),
isRelative: Joi.boolean(),
timeStyle: Joi.string().regex(
/(PKDateStyleNone|PKDateStyleShort|PKDateStyleMedium|PKDateStyleLong|PKDateStyleFull)/,
"date style",
),
// number fields formatters, all optionals
currencyCode: Joi.string().when("value", {
is: Joi.number(),
otherwise: Joi.string().forbidden(),
}),
numberStyle: Joi.string()
.regex(
/(PKNumberStyleDecimal|PKNumberStylePercent|PKNumberStyleScientific|PKNumberStyleSpellOut)/,
)
.when("value", {
is: Joi.number(),
otherwise: Joi.string().forbidden(),
}),
});

244
src/schemas/index.ts Normal file
View File

@@ -0,0 +1,244 @@
export * from "./barcode";
export * from "./beacon";
export * from "./location";
export * from "./field";
export * from "./nfc";
export * from "./semantics";
export * from "./passFields";
export * from "./personalization";
import Joi from "joi";
import debug from "debug";
import { Barcode } from "./barcode";
import { Location } from "./location";
import { Beacon } from "./beacon";
import { NFC } from "./nfc";
import { Field } from "./field";
import { PassFields, TransitType } from "./passFields";
import { Personalization } from "./personalization";
const schemaDebug = debug("Schema");
export interface Manifest {
[key: string]: string;
}
export interface Certificates {
wwdr?: string;
signerCert?: string;
signerKey?:
| {
keyFile: string;
passphrase?: string;
}
| string;
}
export interface FactoryOptions {
model: BundleUnit | string;
certificates: Certificates;
overrides?: OverridesSupportedOptions;
}
export interface BundleUnit {
[key: string]: Buffer;
}
export interface PartitionedBundle {
bundle: BundleUnit;
l10nBundle: {
[key: string]: BundleUnit;
};
}
export interface CertificatesSchema {
wwdr: string;
signerCert: string;
signerKey: string;
}
export const CertificatesSchema = Joi.object<CertificatesSchema>()
.keys({
wwdr: Joi.alternatives(Joi.binary(), Joi.string()).required(),
signerCert: Joi.alternatives(Joi.binary(), Joi.string()).required(),
signerKey: Joi.alternatives()
.try(
Joi.object().keys({
keyFile: Joi.alternatives(
Joi.binary(),
Joi.string(),
).required(),
passphrase: Joi.string().required(),
}),
Joi.alternatives(Joi.binary(), Joi.string()),
)
.required(),
})
.required();
export interface PassInstance {
model: PartitionedBundle;
certificates: CertificatesSchema;
overrides?: OverridesSupportedOptions;
}
export const PassInstance = Joi.object<PassInstance>().keys({
model: Joi.alternatives(Joi.object(), Joi.string()).required(),
certificates: Joi.object(),
overrides: Joi.object(),
});
export interface OverridesSupportedOptions {
serialNumber?: string;
description?: string;
organizationName?: string;
passTypeIdentifier?: string;
teamIdentifier?: string;
appLaunchURL?: string;
associatedStoreIdentifiers?: Array<number>;
userInfo?: { [key: string]: any };
webServiceURL?: string;
authenticationToken?: string;
sharingProhibited?: boolean;
backgroundColor?: string;
foregroundColor?: string;
labelColor?: string;
groupingIdentifier?: string;
suppressStripShine?: boolean;
logoText?: string;
maxDistance?: number;
}
export const OverridesSupportedOptions = Joi.object<OverridesSupportedOptions>()
.keys({
serialNumber: Joi.string(),
description: Joi.string(),
organizationName: Joi.string(),
passTypeIdentifier: Joi.string(),
teamIdentifier: Joi.string(),
appLaunchURL: Joi.string(),
associatedStoreIdentifiers: Joi.array().items(Joi.number()),
userInfo: Joi.alternatives(Joi.object().unknown(), Joi.array()),
// parsing url as set of words and nums followed by dots, optional port and any possible path after
webServiceURL: Joi.string().regex(
/https?:\/\/(?:[a-z0-9]+\.?)+(?::\d{2,})?(?:\/[\S]+)*/,
),
authenticationToken: Joi.string().min(16),
sharingProhibited: Joi.boolean(),
backgroundColor: Joi.string().min(10).max(16),
foregroundColor: Joi.string().min(10).max(16),
labelColor: Joi.string().min(10).max(16),
groupingIdentifier: Joi.string(),
suppressStripShine: Joi.boolean(),
logoText: Joi.string(),
maxDistance: Joi.number().positive(),
})
.with("webServiceURL", "authenticationToken");
export interface ValidPassType {
boardingPass?: PassFields & { transitType: TransitType };
eventTicket?: PassFields;
coupon?: PassFields;
generic?: PassFields;
storeCard?: PassFields;
}
interface PassInterfacesProps {
barcode?: Barcode;
barcodes?: Barcode[];
beacons?: Beacon[];
locations?: Location[];
maxDistance?: number;
relevantDate?: string;
nfc?: NFC;
expirationDate?: string;
voided?: boolean;
}
type AllPassProps = PassInterfacesProps &
ValidPassType &
OverridesSupportedOptions;
export type ValidPass = {
[K in keyof AllPassProps]: AllPassProps[K];
};
export type PassColors = Pick<
OverridesSupportedOptions,
"backgroundColor" | "foregroundColor" | "labelColor"
>;
// --------- UTILITIES ---------- //
type AvailableSchemas =
| typeof Barcode
| typeof Location
| typeof Beacon
| typeof NFC
| typeof Field
| typeof PassFields
| typeof Personalization
| typeof TransitType
| typeof PassInstance
| typeof CertificatesSchema
| typeof OverridesSupportedOptions;
export type ArrayPassSchema = Beacon | Location | Barcode;
/* function resolveSchemaName(name: Schema) {
return schemas[name] || undefined;
}
*/
/**
* Checks if the passed options are compliant with the indicated schema
* @param {any} opts - options to be checks
* @param {string} schemaName - the indicated schema (will be converted)
* @returns {boolean} - result of the check
*/
export function isValid(opts: any, schema: AvailableSchemas): boolean {
if (!schema) {
schemaDebug(
`validation failed due to missing or mispelled schema name`,
);
return false;
}
const validation = schema.validate(opts);
if (validation.error) {
schemaDebug(
`validation failed due to error: ${validation.error.message}`,
);
}
return !validation.error;
}
/**
* Executes the validation in verbose mode, exposing the value or an empty object
* @param {object} opts - to be validated
* @param {*} schemaName - selected schema
* @returns {object} the filtered value or empty object
*/
export function getValidated<T extends Object>(
opts: T,
schema: AvailableSchemas,
): T | null {
if (!schema) {
schemaDebug(`validation failed due to missing schema`);
return null;
}
const validation = schema.validate(opts, { stripUnknown: true });
if (validation.error) {
schemaDebug(
`Validation failed in getValidated due to error: ${validation.error.message}`,
);
return null;
}
return validation.value;
}

15
src/schemas/location.ts Normal file
View File

@@ -0,0 +1,15 @@
import Joi from "joi";
export interface Location {
relevantText?: string;
altitude?: number;
latitude: number;
longitude: number;
}
export const Location = Joi.object<Location>().keys({
altitude: Joi.number(),
latitude: Joi.number().required(),
longitude: Joi.number().required(),
relevantText: Joi.string(),
});

11
src/schemas/nfc.ts Normal file
View File

@@ -0,0 +1,11 @@
import Joi from "joi";
export interface NFC {
message: string;
encryptionPublicKey?: string;
}
export const NFC = Joi.object<NFC>().keys({
message: Joi.string().required().max(64),
encryptionPublicKey: Joi.string(),
});

35
src/schemas/passFields.ts Normal file
View File

@@ -0,0 +1,35 @@
import Joi from "joi";
import { Field } from "./field";
export interface PassFields {
auxiliaryFields: (Field & { row?: number })[];
backFields: Field[];
headerFields: Field[];
primaryFields: Field[];
secondaryFields: Field[];
}
export const PassFields = Joi.object<PassFields>().keys({
auxiliaryFields: Joi.array().items(
Joi.object()
.keys({
row: Joi.number().max(1).min(0),
})
.concat(Field),
),
backFields: Joi.array().items(Field),
headerFields: Joi.array().items(Field),
primaryFields: Joi.array().items(Field),
secondaryFields: Joi.array().items(Field),
});
export type TransitType =
| "PKTransitTypeAir"
| "PKTransitTypeBoat"
| "PKTransitTypeBus"
| "PKTransitTypeGeneric"
| "PKTransitTypeTrain";
export const TransitType = Joi.string().regex(
/(PKTransitTypeAir|PKTransitTypeBoat|PKTransitTypeBus|PKTransitTypeGeneric|PKTransitTypeTrain)/,
);

View File

@@ -0,0 +1,26 @@
import Joi from "joi";
export interface Personalization {
description: string;
requiredPersonalizationFields: RequiredPersonalizationFields[];
termsAndConditions?: string;
}
type RequiredPersonalizationFields =
| "PKPassPersonalizationFieldName"
| "PKPassPersonalizationFieldPostalCode"
| "PKPassPersonalizationFieldEmailAddress"
| "PKPassPersonalizationFieldPhoneNumber";
export const Personalization = Joi.object<Personalization>().keys({
description: Joi.string().required(),
requiredPersonalizationFields: Joi.array()
.items(
"PKPassPersonalizationFieldName",
"PKPassPersonalizationFieldPostalCode",
"PKPassPersonalizationFieldEmailAddress",
"PKPassPersonalizationFieldPhoneNumber",
)
.required(),
termsAndConditions: Joi.string(),
});

267
src/schemas/semantics.ts Normal file
View File

@@ -0,0 +1,267 @@
import Joi from "joi";
/**
* For a better description of every single field,
* please refer to Apple official documentation.
*
* @see https://developer.apple.com/documentation/walletpasses/semantictags
*/
/**
* @see https://developer.apple.com/documentation/walletpasses/semantictagtype
*/
declare namespace SemanticTagType {
interface PersonNameComponents {
familyName?: string;
givenName?: string;
middleName?: string;
namePrefix?: string;
nameSuffix?: string;
nickname?: string;
phoneticRepresentation?: string;
}
interface CurrencyAmount {
currencyCode?: string; // ISO 4217 currency code
amount?: string;
}
interface Location {
latitude: number;
longitude: number;
}
interface Seat {
seatSection?: string;
seatRow?: string;
seatNumber?: string;
seatIdentifier?: string;
seatType?: string;
seatDescription?: string;
}
interface WifiNetwork {
password: string;
ssid: string;
}
}
const CurrencyAmount = Joi.object<SemanticTagType.CurrencyAmount>().keys({
currencyCode: Joi.string(),
amount: Joi.string(),
});
const PersonNameComponent = Joi.object<SemanticTagType.PersonNameComponents>().keys(
{
givenName: Joi.string(),
familyName: Joi.string(),
middleName: Joi.string(),
namePrefix: Joi.string(),
nameSuffix: Joi.string(),
nickname: Joi.string(),
phoneticRepresentation: Joi.string(),
},
);
const seat = Joi.object<SemanticTagType.Seat>().keys({
seatSection: Joi.string(),
seatRow: Joi.string(),
seatNumber: Joi.string(),
seatIdentifier: Joi.string(),
seatType: Joi.string(),
seatDescription: Joi.string(),
});
const location = Joi.object<SemanticTagType.Location>().keys({
latitude: Joi.number().required(),
longitude: Joi.number().required(),
});
const WifiNetwork = Joi.object<SemanticTagType.WifiNetwork>().keys({
password: Joi.string().required(),
ssid: Joi.string().required(),
});
/**
* Alphabetical order
* @see https://developer.apple.com/documentation/walletpasses/semantictags
*/
export interface Semantics {
airlineCode?: string;
artistIDs?: string[];
awayTeamAbbreviation?: string;
awayTeamLocation?: string;
awayTeamName?: string;
balance?: SemanticTagType.CurrencyAmount;
boardingGroup?: string;
boardingSequenceNumber?: string;
carNumber?: string;
confirmationNumber?: string;
currentArrivalDate?: string;
currentBoardingDate?: string;
currentDepartureDate?: string;
departureAirportCode?: string;
departureAirportName?: string;
departureGate?: string;
departureLocation?: SemanticTagType.Location;
departureLocationDescription?: string;
departurePlatform?: string;
departureStationName?: string;
departureTerminal?: string;
destinationAirportCode?: string;
destinationAirportName?: string;
destinationGate?: string;
destinationLocation?: SemanticTagType.Location;
destinationLocationDescription?: string;
destinationPlatform?: string;
destinationStationName?: string;
destinationTerminal?: string;
duration?: number;
eventEndDate?: string;
eventName?: string;
eventStartDate?: string;
eventType?:
| "PKEventTypeGeneric"
| "PKEventTypeLivePerformance"
| "PKEventTypeMovie"
| "PKEventTypeSports"
| "PKEventTypeConference"
| "PKEventTypeConvention"
| "PKEventTypeWorkshop"
| "PKEventTypeSocialGathering";
flightCode?: string;
flightNumber?: number;
genre?: string;
homeTeamAbbreviation?: string;
homeTeamLocation?: string;
homeTeamName?: string;
leagueAbbreviation?: string;
leagueName?: string;
membershipProgramName?: string;
membershipProgramNumber?: string;
originalArrivalDate?: string;
originalBoardingDate?: string;
originalDepartureDate?: string;
passengerName?: SemanticTagType.PersonNameComponents;
performerNames?: string[];
priorityStatus?: string;
seats?: SemanticTagType.Seat[];
securityScreening?: string;
silenceRequested?: boolean;
sportName?: string;
totalPrice?: SemanticTagType.CurrencyAmount;
transitProvider?: string;
transitStatus?: string;
transitStatusReason?: string;
vehicleName?: string;
vehicleNumber?: string;
vehicleType?: string;
venueEntrance?: string;
venueLocation?: SemanticTagType.Location;
venueName?: string;
venuePhoneNumber?: string;
venueRoom?: string;
wifiAccess?: SemanticTagType.WifiNetwork;
}
export const Semantics = Joi.object<Semantics>().keys({
airlineCode: Joi.string(),
artistIDs: Joi.array().items(Joi.string()),
awayTeamAbbreviation: Joi.string(),
awayTeamLocation: Joi.string(),
awayTeamName: Joi.string(),
balance: CurrencyAmount,
boardingGroup: Joi.string(),
boardingSequenceNumber: Joi.string(),
carNumber: Joi.string(),
confirmationNumber: Joi.string(),
currentArrivalDate: Joi.string(),
currentBoardingDate: Joi.string(),
currentDepartureDate: Joi.string(),
departureAirportCode: Joi.string(),
departureAirportName: Joi.string(),
departureGate: Joi.string(),
departureLocation: location,
departureLocationDescription: Joi.string(),
departurePlatform: Joi.string(),
departureStationName: Joi.string(),
departureTerminal: Joi.string(),
destinationAirportCode: Joi.string(),
destinationAirportName: Joi.string(),
destinationGate: Joi.string(),
destinationLocation: location,
destinationLocationDescription: Joi.string(),
destinationPlatform: Joi.string(),
destinationStationName: Joi.string(),
destinationTerminal: Joi.string(),
duration: Joi.number(),
eventEndDate: Joi.string(),
eventName: Joi.string(),
eventStartDate: Joi.string(),
eventType: Joi.string().regex(
/(PKEventTypeGeneric|PKEventTypeLivePerformance|PKEventTypeMovie|PKEventTypeSports|PKEventTypeConference|PKEventTypeConvention|PKEventTypeWorkshop|PKEventTypeSocialGathering)/,
),
flightCode: Joi.string(),
flightNumber: Joi.number(),
genre: Joi.string(),
homeTeamAbbreviation: Joi.string(),
homeTeamLocation: Joi.string(),
homeTeamName: Joi.string(),
leagueAbbreviation: Joi.string(),
leagueName: Joi.string(),
membershipProgramName: Joi.string(),
membershipProgramNumber: Joi.string(),
originalArrivalDate: Joi.string(),
originalBoardingDate: Joi.string(),
originalDepartureDate: Joi.string(),
passengerName: PersonNameComponent,
performerNames: Joi.array().items(Joi.string()),
priorityStatus: Joi.string(),
seats: Joi.array().items(seat),
securityScreening: Joi.string(),
silenceRequested: Joi.boolean(),
sportName: Joi.string(),
totalPrice: CurrencyAmount,
transitProvider: Joi.string(),
transitStatus: Joi.string(),
transitStatusReason: Joi.string(),
vehicleName: Joi.string(),
vehicleNumber: Joi.string(),
vehicleType: Joi.string(),
venueEntrance: Joi.string(),
venueLocation: location,
venueName: Joi.string(),
venuePhoneNumber: Joi.string(),
venueRoom: Joi.string(),
wifiAccess: Joi.array().items(WifiNetwork),
});