Skip to content

Commit

Permalink
split out api schema loading
Browse files Browse the repository at this point in the history
  • Loading branch information
bradbanister committed Oct 17, 2023
1 parent 62d4cb2 commit 654abfa
Show file tree
Hide file tree
Showing 15 changed files with 337 additions and 288 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: Apache-2.0
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

import path from 'path';
import fs from 'fs-extra';
import { writeDebugMessage, writeErrorToLog } from '../Logger';
import { ApiSchema, NoApiSchema } from '../model/api-schema/ApiSchema';
import { TraceId } from '../model/IdTypes';
import { ProjectNamespace } from '../model/api-schema/ProjectNamespace';

const moduleName = 'ApiSchemaFinder';

const dataStandard50pre1Path: string = path.resolve(__dirname, '../ds-schemas/DataStandard-5.0.0-pre.1.json');

export const projectNamespaceEdfi: ProjectNamespace = 'ed-fi' as ProjectNamespace;

/**
* This is a simple cache implementation that works in Lambdas, see: https://rewind.io/blog/simple-caching-in-aws-lambda-functions/
*/
let apiSchemaCache: ApiSchema | null = null;

/**
* Loads ApiSchema from an ApiSchema JSON file
*/
async function loadApiSchemaFromFile(filePath: string, traceId: TraceId): Promise<ApiSchema> {
const apiSchema: ApiSchema = (await fs.readJson(filePath)) as ApiSchema;
apiSchemaCache = apiSchema;
writeDebugMessage(moduleName, 'loadApiSchemaFromFile', traceId, `Loading ApiSchema from ${filePath}`);
return apiSchema;
}

/**
* Entry point for loading ApiSchemas from a file
*/
export async function findApiSchema(traceId: TraceId): Promise<ApiSchema> {
if (apiSchemaCache != null) return apiSchemaCache;

try {
return await loadApiSchemaFromFile(dataStandard50pre1Path, traceId);
} catch (e) {
writeErrorToLog(moduleName, traceId, 'getApiSchema', e);
}
return NoApiSchema;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,125 +3,25 @@
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

import path from 'path';
import fs from 'fs-extra';
import { Logger } from '@edfi/meadowlark-utilities';
import type { TraceId } from '../model/IdTypes';
import { NoProjectSchema, ProjectSchema } from '../model/api-schema/ProjectSchema';
import { writeDebugMessage, writeErrorToLog } from '../Logger';
import type { ApiSchema } from '../model/api-schema/ApiSchema';
import { TraceId } from '../model/IdTypes';
import { EndpointName } from '../model/api-schema/EndpointName';
import { ApiSchema } from '../model/api-schema/ApiSchema';
import { ProjectNamespace } from '../model/api-schema/ProjectNamespace';
import { ProjectShortVersion } from '../model/ProjectShortVersion';
import { SemVer } from '../model/api-schema/SemVer';

const moduleName = 'LoadProjectSchema';

const dataStandard33bPath: string = path.resolve(__dirname, '../ds-schemas/DataStandard-3.3.1-b.json');
const dataStandard50pre1Path: string = path.resolve(__dirname, '../ds-schemas/DataStandard-5.0.0-pre.1.json');

const projectNamespaceEdfi: ProjectNamespace = 'ed-fi' as ProjectNamespace;

export const projectShortVersion33b: ProjectShortVersion = 'v3.3b' as ProjectShortVersion;
const projectShortVersion50pre1: ProjectShortVersion = 'v5.0-pre.1' as ProjectShortVersion;

/**
* This is a simple cache implementation that works in Lambdas, see: https://rewind.io/blog/simple-caching-in-aws-lambda-functions/
*/
const projectSchemaCache = {};

function cacheKeyFrom(projectNamespace: ProjectNamespace, projectShortVersion: ProjectShortVersion): string {
return `${projectNamespace}|${projectShortVersion}`;
}

function setProjectSchemaInCache(
projectNamespace: ProjectNamespace,
projectShortVersion: ProjectShortVersion,
apiSchema: ProjectSchema,
) {
projectSchemaCache[cacheKeyFrom(projectNamespace, projectShortVersion)] = apiSchema;
}

function getProjectSchemaFromCache(
projectNamespace: ProjectNamespace,
projectShortVersion: ProjectShortVersion,
): ProjectSchema | undefined {
return projectSchemaCache[cacheKeyFrom(projectNamespace, projectShortVersion)];
}

/**
* Loads ProjectSchema from an ApiSchema JSON file
*/
async function loadProjectSchemaFromFile(
projectNamespace: ProjectNamespace,
projectShortVersion: ProjectShortVersion,
filePath: string,
traceId: TraceId,
): Promise<ProjectSchema> {
const projectSchema: ProjectSchema = ((await fs.readJson(filePath)) as ApiSchema).projectSchemas[projectNamespace];
setProjectSchemaInCache(projectNamespace, projectShortVersion, projectSchema);
writeDebugMessage(moduleName, 'loadProjectSchema', traceId, `Loading ApiSchema from ${filePath}`);
return projectSchema;
}

/**
* Entry point for loading ProjectSchemas from a file
* Finds the ProjectSchema that represents the given ProjectNamespace.
*/
export async function findProjectSchema(
apiSchema: ApiSchema,
projectNamespace: ProjectNamespace,
projectShortVersion: ProjectShortVersion,
traceId: TraceId,
): Promise<ProjectSchema> {
const cachedProjectSchema: ProjectSchema | undefined = getProjectSchemaFromCache(projectNamespace, projectShortVersion);
if (cachedProjectSchema != null) return cachedProjectSchema;
const projectSchema: ProjectSchema | undefined = apiSchema.projectSchemas[projectNamespace];

// Only supporting data standards right now
if (projectNamespace !== projectNamespaceEdfi) {
writeDebugMessage(moduleName, 'getProjectSchema', traceId, `Namespace ${projectNamespace} on URL is unknown.`);
if (projectSchema == null) {
Logger.warn(`findProjectSchema: projectNamespace '${projectNamespace}' does not exist in ApiSchema`, traceId);
return NoProjectSchema;
}

try {
if (projectShortVersion === projectShortVersion33b) {
return await loadProjectSchemaFromFile(projectNamespaceEdfi, projectShortVersion33b, dataStandard33bPath, traceId);
}
if (projectShortVersion === projectShortVersion50pre1) {
return await loadProjectSchemaFromFile(
projectNamespaceEdfi,
projectShortVersion50pre1,
dataStandard50pre1Path,
traceId,
);
}

writeDebugMessage(moduleName, 'getProjectSchema', traceId, `Version ${projectShortVersion} on URL is unknown.`);
} catch (e) {
writeErrorToLog(moduleName, traceId, 'getProjectSchema', e);
}
return NoProjectSchema;
}

/**
* Returns a list of the valid endpoint names for a ProjectSchema.
*/
export function validEndpointNamesFor(
projectNamespace: ProjectNamespace,
projectShortVersion: ProjectShortVersion,
): EndpointName[] {
const projectSchema: ProjectSchema | undefined = getProjectSchemaFromCache(projectNamespace, projectShortVersion);
if (projectSchema == null) return [];
return Object.keys(projectSchema.resourceSchemas) as EndpointName[];
}

/**
* Returns the hardcoded ProjectShortVersion for a given project semver.
*/
export function versionAbbreviationFor(projectVersion: SemVer): ProjectShortVersion {
switch (projectVersion) {
case '3.3.1-b':
return projectShortVersion33b;
case '5.0.0-pre.1':
return projectShortVersion50pre1;
default:
return '' as ProjectShortVersion;
}
return projectSchema;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,19 @@

import type { PathComponents } from '../model/PathComponents';
import type { ResourceSchema } from '../model/api-schema/ResourceSchema';
import { findProjectSchema } from './ProjectSchemaFinder';
import type { TraceId } from '../model/IdTypes';
import { ProjectSchema } from '../model/api-schema/ProjectSchema';
import { ApiSchema } from '../model/api-schema/ApiSchema';
import { findProjectSchema } from './ProjectSchemaFinder';

/**
* Finds the ResourceSchema that represents the given REST resource path.
*/
export async function findResourceSchema(
apiSchema: ApiSchema,
pathComponents: PathComponents,
traceId: TraceId,
): Promise<ResourceSchema | undefined> {
const projectSchema: ProjectSchema = await findProjectSchema(
pathComponents.projectNamespace,
pathComponents.projectShortVersion,
traceId,
);
const projectSchema: ProjectSchema = await findProjectSchema(apiSchema, pathComponents.projectNamespace, traceId);
return projectSchema.resourceSchemas[pathComponents.endpointName];
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import { SuperclassInfo } from '../model/SuperclassInfo';
import { DocumentIdentity } from '../model/DocumentIdentity';
import { ResourceSchema } from '../model/api-schema/ResourceSchema';
import { deriveSuperclassInfoFrom } from './SuperclassInfoExtractor';
import { ApiSchema } from '../model/api-schema/ApiSchema';

/**
* Extracts document identity and document reference information from the request body.
*/
export async function extractDocumentInfo(
apiSchema: ApiSchema,
resourceSchema: ResourceSchema,
body: object,
requestTimestamp: number,
Expand All @@ -28,7 +30,7 @@ export async function extractDocumentInfo(
}

return {
documentReferences: extractDocumentReferences(resourceSchema, body),
documentReferences: extractDocumentReferences(apiSchema, resourceSchema, body),
descriptorReferences: extractDescriptorValues(resourceSchema, body),
documentIdentity,
superclassInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ import { DocumentObjectKey } from '../model/api-schema/DocumentObjectKey';
import { DocumentPaths } from '../model/api-schema/DocumentPaths';
import { ResourceSchema } from '../model/api-schema/ResourceSchema';
import { DocumentIdentity } from '../model/DocumentIdentity';
import { ApiSchema } from '../model/api-schema/ApiSchema';
import { MetaEdProjectName } from '../model/api-schema/MetaEdProjectName';
import { MetaEdResourceName } from '../model/api-schema/MetaEdResourceName';
import { ProjectSchema } from '../model/api-schema/ProjectSchema';

/**
* In extracting DocumentReferences, there is an intermediate step where document values are resolved
* from a JsonPath. JsonPaths return arrays of values when the path goes into an array.
* This is the case for collections of document references.
*
* This means that each path resolves to one document value in *each* document reference in the collection.
* For each DocumentObjectKey of a reference, IntermediateDocumentReferences holds the array of resolved document values
* For each DocumentObjectKey of a reference, IntermediateDocumentIdentities holds the array of resolved document values
* for a path.
*
* For example, given a document with a collection of ClassPeriod references:
Expand All @@ -39,23 +43,51 @@ import { DocumentIdentity } from '../model/DocumentIdentity';
* With JsonPaths for ClassPeriod references:
* "* $.classPeriods[*].classPeriodReference.schoolId" for schoolId and
* "$.classPeriods[*].classPeriodReference.classPeriodName" for classPeriodName,
* the IntermediateDocumentReferences would be:
* the IntermediateDocumentIdentities would be:
*
* {
* schoolId: ['24', '25'],
* classPeriodName: ['z1', 'z2']
* }
*
* IntermediateDocumentReferences here contains information for two DocumentReferences, but as "slices" in the wrong
* IntermediateDocumentIdentities here contains information for two DocumentIdentities, but as "slices" in the wrong
* orientation.
*/
type IntermediateDocumentReferences = { [key: DocumentObjectKey]: any[] };
type IntermediateDocumentIdentities = { [key: DocumentObjectKey]: any[] };

/**
* All the information in a DocumentIdentity but as an object, meaning the keys are unsorted
*/
type UnsortedDocumentIdentity = { [key: DocumentObjectKey]: any };

/**
* Finds the ResourceSchema for the document reference
*/
function resourceSchemaForReference(
apiSchema: ApiSchema,
projectName: MetaEdProjectName,
resourceName: MetaEdResourceName,
): ResourceSchema {
const projectSchema: ProjectSchema | undefined = Object.values(apiSchema.projectSchemas).find(
(ps) => ps.projectName === projectName,
);
invariant(projectSchema != null, `Project schema with projectName ${projectName} was not found`);
const result: ResourceSchema | undefined = Object.values(projectSchema.resourceSchemas).find(
(resourceSchema) => resourceSchema.resourceName === resourceName,
);
invariant(result != null, `Resource schema with resourceName ${resourceName} was not found`);
return result;
}

/**
* Takes a resource schema and an API document for that resource and
* extracts the document reference information from the document.
*/
export function extractDocumentReferences(resourceSchema: ResourceSchema, documentBody: object): DocumentReference[] {
export function extractDocumentReferences(
apiSchema: ApiSchema,
resourceSchema: ResourceSchema,
documentBody: object,
): DocumentReference[] {
const result: DocumentReference[] = [];

Object.values(resourceSchema.documentPathsMapping).forEach((documentPaths: DocumentPaths) => {
Expand All @@ -66,7 +98,7 @@ export function extractDocumentReferences(resourceSchema: ResourceSchema, docume
if (documentPaths.isDescriptor) return;

// Build up intermediateDocumentReferences
const intermediateDocumentReferences: IntermediateDocumentReferences = {};
const intermediateDocumentReferences: IntermediateDocumentIdentities = {};
Object.entries(documentPaths.paths).forEach(([documentKey, documentJsonPath]) => {
const documentValuesSlice: any[] = jsonPath({
path: documentJsonPath,
Expand Down Expand Up @@ -98,13 +130,29 @@ export function extractDocumentReferences(resourceSchema: ResourceSchema, docume
),
);

// Look up identityPathOrder for this reference
const referenceResourceSchema: ResourceSchema = resourceSchemaForReference(
apiSchema,
documentPaths.projectName,
documentPaths.resourceName,
);

// Reorient intermediateDocumentReferences into actual references
for (let index = 0; index < documentValuesSlices[0].length; index += 1) {
const documentIdentity: DocumentIdentity = [];
const unsortedDocumentIdentity: UnsortedDocumentIdentity = {};

// Build the document identity in the correct path order
documentPaths.pathOrder.forEach((documentKey: DocumentObjectKey) => {
documentIdentity.push({ documentKey, documentValue: intermediateDocumentReferences[documentKey][index] });
unsortedDocumentIdentity[documentKey] = intermediateDocumentReferences[documentKey][index];
});

const documentIdentity: DocumentIdentity = [];

referenceResourceSchema.identityPathOrder.forEach((documentKey: DocumentObjectKey) => {
documentIdentity.push({
documentKey,
documentValue: unsortedDocumentIdentity[documentKey],
});
});

result.push({
Expand Down
Loading

0 comments on commit 654abfa

Please sign in to comment.