Skip to content

Commit

Permalink
Flightplan API
Browse files Browse the repository at this point in the history
  • Loading branch information
pbaer committed Mar 30, 2024
1 parent f40e348 commit bf50137
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 6 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ To run locally:

In the web folder: `npm start`
In the api folder: `npm start` and `npm run watch` in separate terminals
In the temp folder: `azurite --location .\azurite --debug azurite-debug.log`

Use Azure Storage Explorer to inspect local blob storage
3 changes: 1 addition & 2 deletions api/airport/function.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
"get"
]
},
{
Expand Down
70 changes: 70 additions & 0 deletions api/flightplan/flightplan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Ajv from 'ajv';
import addFormats from 'ajv-formats';

const flightPlanSchema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"id": {
"type": "string"
},
"departureTime": {
"type": "string",
"format": "date-time"
},
"aircraft": {
"type": "string"
},
"aircraftType": {
"type": "string"
},
"segments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"route": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 2
},
"altitude": {
"type": "number",
"minimum": 1,
"maximum": 60000
},
"remarks": {
"type": "string"
}
},
"required": ["route", "altitude"]
}
}
},
"required": ["departureTime", "aircraft", "aircraftType", "segments"]
};

export interface ISegment {
route: string[];
altitude: number;
remarks?: string;
}

export interface IFlightPlan {
id?: string;
departureTime: string;
aircraft: string;
aircraftType: string;
segments: ISegment[];
}

export const validateFlightPlan = (flightPlan: IFlightPlan) => {
const ajv = new Ajv();
addFormats(ajv);
const validateFlightPlanSchema = ajv.compile(flightPlanSchema);
if (!validateFlightPlanSchema(flightPlan)) {
throw new Error(`Schema validation failed: ${validateFlightPlanSchema.errors}`);
}
};
22 changes: 22 additions & 0 deletions api/flightplan/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post",
"put"
],
"route": "flightplan/{id?}"
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../dist/flightplan/index.js"
}
129 changes: 129 additions & 0 deletions api/flightplan/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { AzureFunction, Context, HttpRequest } from "@azure/functions"
import { v4 as uuidv4 } from 'uuid';
import BlobStorage from "../shared/blob-storage";
import { validateFlightPlan } from "./flightplan";

const makeBlobKey = (id: string) => `${id}.json`;

const flightPlanHttpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
const blobStorage = new BlobStorage(process.env.BLOB_STORAGE_CONNECTION!, 'flightplans');

switch (req.method) {
case "POST":
{
let flightPlanString: string;
try {
const flightPlan = req.body;
validateFlightPlan(flightPlan);
flightPlan.id = uuidv4();
flightPlanString = JSON.stringify(flightPlan, null, 2);
blobStorage.writeBlob(makeBlobKey(flightPlan.id), flightPlanString);
} catch(err) {
context.res = {
status: 400,
body: `Invalid flight plan: ${err}`,
};
return;
}
context.res = {
status: 201,
body: flightPlanString,
};
}
break;
case "PUT":
{
let flightPlanBlobKeys: string[];
try {
flightPlanBlobKeys = await blobStorage.listBlobs();
} catch(err) {
context.res = {
status: 500,
body: `Error listing flight plans`,
};
return;
}
if (!flightPlanBlobKeys.includes(makeBlobKey(context.bindingData.id))) {
context.res = {
status: 404,
body: `Unknown flight plan ${context.bindingData.id}`,
};
return;
}
let flightPlanString: string;
try {
const flightPlan = req.body;
validateFlightPlan(flightPlan);
flightPlan.id = context.bindingData.id;
flightPlanString = JSON.stringify(flightPlan, null, 2);
blobStorage.writeBlob(makeBlobKey(flightPlan.id), flightPlanString);
} catch(err) {
context.res = {
status: 400,
body: `Invalid flight plan: ${err}`,
};
return;
}
context.res = {
status: 200,
body: flightPlanString,
};
}
break;
case "GET":
{
if (context.bindingData.id) {
let flightPlanString: string;
try {
flightPlanString = await blobStorage.readBlob(makeBlobKey(context.bindingData.id));
} catch(err) {
context.res = {
status: 404,
body: `Unknown flight plan ${context.bindingData.id}`,
};
return;
}
context.res = {
status: 200,
body: flightPlanString
};
} else {
let flightPlanBlobKeys: string[];
try {
flightPlanBlobKeys = await blobStorage.listBlobs();
} catch(err) {
context.res = {
status: 500,
body: `Error listing flight plans`,
};
return;
}
let flightPlans: string[] = [];
for (let flightPlanBlobKey of flightPlanBlobKeys) {
try {
flightPlans.push(await blobStorage.readBlob(flightPlanBlobKey));
} catch(err) {
context.res = {
status: 500,
body: `Error reading flight plan ${flightPlanBlobKey}`,
};
return;
}
}
context.res = {
status: 200,
body: `[${flightPlans.join(',\n')}]`
};
}
}
break;
default:
context.res = {
status: 400,
body: "Invalid HTTP method",
};
break;
}
};

export default flightPlanHttpTrigger;
6 changes: 5 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@
"test": "echo \"No tests yet...\""
},
"dependencies": {
"@azure/storage-blob": "^12.17.0",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"dotenv": "^16.3.1",
"metar-taf-parser": "^8.0.4",
"node-fetch": "^3.3.2",
"sunrise-sunset-js": "^2.2.1"
"sunrise-sunset-js": "^2.2.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"@azure/functions": "^3.0.0",
Expand Down
53 changes: 53 additions & 0 deletions api/shared/blob-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { BlobServiceClient, ContainerClient, BlobClient } from '@azure/storage-blob';

class BlobStorage {
private containerClient: ContainerClient;

constructor(connectionString: string, containerName: string) {
const serviceClient = BlobServiceClient.fromConnectionString(connectionString);
this.containerClient = serviceClient.getContainerClient(containerName);
}

async listBlobs(): Promise<string[]> {
let blobNames: string[] = [];
for await (const blob of this.containerClient.listBlobsFlat()) {
blobNames.push(blob.name);
}
return blobNames;
}

async readBlob(key: string): Promise<string> {
const blobClient = this.containerClient.getBlobClient(key);
const downloadBlockBlobResponse = await blobClient.download();
if (!downloadBlockBlobResponse.readableStreamBody) {
throw new Error('No blob stream found');
}
const content = await this.streamToString(downloadBlockBlobResponse.readableStreamBody);
return content;
}

async writeBlob(key: string, value: string): Promise<void> {
const blobClient = this.containerClient.getBlockBlobClient(key);
await blobClient.upload(value, value.length);
}

async deleteBlob(key: string): Promise<void> {
const blobClient = this.containerClient.getBlobClient(key);
await blobClient.delete();
}

private async streamToString(readableStream: NodeJS.ReadableStream): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: string[] = [];
readableStream.on('data', (data) => {
chunks.push(data.toString());
});
readableStream.on('end', () => {
resolve(chunks.join(''));
});
readableStream.on('error', reject);
});
}
}

export default BlobStorage;
1 change: 0 additions & 1 deletion api/warmup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const warmupHttpTrigger: AzureFunction = async function (context: Context, req:
context.res = {
body: "Warmup completed"
};

};

export default warmupHttpTrigger;
3 changes: 1 addition & 2 deletions api/weather/function.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
"get"
]
},
{
Expand Down

0 comments on commit bf50137

Please sign in to comment.