Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Holiday Grab Bag #466

Merged
merged 15 commits into from
Jan 2, 2025
1 change: 1 addition & 0 deletions api/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
data
data-dev/
4 changes: 4 additions & 0 deletions api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'node:path';
import cors from 'cors';
import express from 'express';
import SwaggerUI from 'swagger-ui-express';
import Bulldozer from './lib/initialization.js';
import history, {Context} from 'connect-history-api-fallback';
import Schema from '@openaddresses/batch-schema';
import { ProfileConnConfig } from './lib/connection-config.js';
Expand Down Expand Up @@ -71,6 +72,9 @@ export default async function server(config: Config) {
console.log(`ok - failed to flush cache: ${err instanceof Error? err.message : String(err)}`);
}

// If the database is empty, populate it with generally sensible defaults
await Bulldozer.fireItUp(config);

await config.conns.init();

if (!config.noevents) await config.events.init(config.pg);
Expand Down
8 changes: 7 additions & 1 deletion api/lib/api/credentials.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import TAKAPI, {
APIAuthPassword
} from '../tak-api.js';
import { Static, Type } from '@sinclair/typebox';
import pem from 'pem';
import xml2js from 'xml2js';

export const CertificateResponse = Type.Object({
cert: Type.String(),
key: Type.String()
});

export default class {
api: TAKAPI;

Expand All @@ -18,7 +24,7 @@ export default class {
});
}

async generate() {
async generate(): Promise<Static<typeof CertificateResponse>> {
if (!(this.api.auth instanceof APIAuthPassword)) throw new Error('Must use Password Auth');

const config = await xml2js.parseStringPromise(await this.config());
Expand Down
106 changes: 106 additions & 0 deletions api/lib/initialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import Config from './config.js';
import path from 'node:path';
import {
Basemap as BasemapParser,
Iconset as IconsetParser,
DataPackage
} from '@tak-ps/node-cot'
import fs from 'node:fs/promises';

/**
* Break ground on populating an empty database
*/
export default class Bulldozer {
config: Config;

constructor(config: Config) {
this.config = config;
}

static async fireItUp(config: Config): Promise<void> {
const bulldozer = new Bulldozer(config);

await Promise.allSettled([
bulldozer.populateIconsets(),
bulldozer.populateBasemaps()
]);
}

async populateIconsets(): Promise<void> {
const count = await this.config.models.Iconset.count();
if (count === 0) {
try {
await fs.access(new URL('../data/', import.meta.url));

for (const file of await fs.readdir(new URL('../data/iconsets/', import.meta.url))) {
console.error(`ok - loading iconset ${file}`);

const dp = await DataPackage.parse(new URL(`../data/iconsets/${file}`, import.meta.url), {
strict: false
});

const files = await dp.files();
if (!files.has('iconset.xml')) continue;

const lookup = new Map();
for (const icon of files) {
lookup.set(path.parse(icon).base, icon);
}

const iconset = await IconsetParser.parse(await dp.getFileBuffer('iconset.xml'));
await this.config.models.Iconset.generate(iconset.to_json())

for (const icon of iconset.icons()) {
const file = lookup.get(icon.name) || icon.name;

if (!files.has(file)) {
console.log(`not ok - could not find ${icon.name} in ${iconset.name}, skipping`);
continue;
}

const iconBuff = await dp.getFileBuffer(file)

if (!iconBuff) {
console.log(`not ok - could not open ${icon.name} in ${iconset.name}, skipping`);
continue;
}

this.config.models.Icon.generate({
iconset: iconset.uid,
name: file,
type2525b: icon.type2525b,
data: iconBuff.toString('base64'),
path: `${iconset.uid}/${file}`
})
}
}
} catch (err) {
console.log('Could not automatically load iconsets', err);
}
}

}

async populateBasemaps(): Promise<void> {
const count = await this.config.models.Basemap.count();
if (count === 0) {
try {
await fs.access(new URL('../data/', import.meta.url));

for (const file of await fs.readdir(new URL('../data/basemaps/', import.meta.url))) {
console.error(`ok - loading basemap ${file}`);
const b = (await BasemapParser.parse(String(await fs.readFile(new URL(`../data/basemaps/${file}`, import.meta.url))))).to_json();

await this.config.models.Basemap.generate({
name: b.name || 'Unknown',
url: b.url,
minzoom: b.minZoom || 0,
maxzoom: b.maxZoom || 16
})
}
} catch (err) {
console.log('Could not automatically load basemaps', err);
}
}
}
}
2 changes: 2 additions & 0 deletions api/lib/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default class Models {

Profile: Modeler<typeof pgtypes.Profile>;
ProfileChat: ProfileChat;
ProfileInterest: Modeler<typeof pgtypes.ProfileInterest>;
ProfileFeature: Modeler<typeof pgtypes.ProfileFeature>;
ProfileOverlay: Modeler<typeof pgtypes.ProfileOverlay>;
ProfileMission: Modeler<typeof pgtypes.ProfileMission>;
Expand All @@ -44,6 +45,7 @@ export default class Models {
this.Setting = new Setting(pg);
this.Server = new Modeler(pg, pgtypes.Server);
this.Profile = new Modeler(pg, pgtypes.Profile);
this.ProfileInterest = new Modeler(pg, pgtypes.ProfileInterest);
this.ProfileFeature = new Modeler(pg, pgtypes.ProfileFeature);
this.ProfileOverlay = new Modeler(pg, pgtypes.ProfileOverlay);
this.ProfileMission = new Modeler(pg, pgtypes.ProfileMission);
Expand Down
8 changes: 8 additions & 0 deletions api/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,14 @@ export const ConnectionToken = pgTable('connection_tokens', {
updated: timestamp({ withTimezone: true, mode: 'string' }).notNull().default(sql`Now()`),
});

export const ProfileInterest = pgTable('profile_interests', {
id: serial().primaryKey(),
name: text().notNull(),
bounds: geometry({ type: GeometryType.Polygon, srid: 4326 }).$type<Polygon>(),
created: timestamp({ withTimezone: true, mode: 'string' }).notNull().default(sql`Now()`),
updated: timestamp({ withTimezone: true, mode: 'string' }).notNull().default(sql`Now()`),
});

export const ProfileMission = pgTable('profile_missions', {
id: serial().primaryKey(),
name: text().notNull(),
Expand Down
4 changes: 4 additions & 0 deletions api/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ export const LayerTemplateResponse = createSelectSchema(schemas.LayerTemplate, {
config: Type.Unknown(),
});

export const ProfileInterestResponse = createSelectSchema(schemas.ProfileInterest, {
id: Type.Integer(),
});

export const ProfileFeature = Type.Composite([ Feature.Feature, Type.Object({
path: Type.String({ default: '/' }),
})]);
Expand Down
7 changes: 7 additions & 0 deletions api/migrations/0081_fluffy_loners.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE "profile_interests" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"bounds" GEOMETRY(POLYGON, 4326),
"created" timestamp with time zone DEFAULT Now() NOT NULL,
"updated" timestamp with time zone DEFAULT Now() NOT NULL
);
Loading
Loading