Skip to content

Commit

Permalink
Merge pull request #221 from DestinyItemManager/stately
Browse files Browse the repository at this point in the history
Initial Stately schema, generated code, and usage in GlobalSettings
  • Loading branch information
bhollis authored Sep 3, 2024
2 parents 3bd48b2 + e3c1cc0 commit 84b65b9
Show file tree
Hide file tree
Showing 31 changed files with 2,601 additions and 35 deletions.
11 changes: 8 additions & 3 deletions .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on: pull_request
jobs:
build:
runs-on: ubuntu-latest
environment: 'test'

services:
postgres:
Expand Down Expand Up @@ -52,8 +53,12 @@ jobs:
- name: Build API
run: pnpm build:api

- name: Test
run: pnpm test

- name: Lint
run: pnpm run lint-check

- name: Test
run: pnpm test
env:
STATELY_STORE_ID: ${{ vars.STATELY_STORE_ID}}
STATELY_CLIENT_ID: ${{ vars.STATELY_CLIENT_ID }}
STATELY_CLIENT_SECRET: ${{ secrets.STATELY_CLIENT_SECRET }}
2 changes: 1 addition & 1 deletion api/routes/auth-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import asyncHandler from 'express-async-handler';
import util from 'util';
import { AuthTokenRequest, AuthTokenResponse } from '../shapes/auth.js';

import jwt, { Secret, SignOptions } from 'jsonwebtoken';
import jwt, { type Secret, type SignOptions } from 'jsonwebtoken';
import _ from 'lodash';
import { metrics } from '../metrics/index.js';
import { ApiApp } from '../shapes/app.js';
Expand Down
42 changes: 33 additions & 9 deletions api/routes/platform-info.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
import { keyPath } from '@stately-cloud/client';
import asyncHandler from 'express-async-handler';
import { pool } from '../db/index.js';
import { defaultGlobalSettings, GlobalSettings } from '../shapes/global-settings.js';
import { client } from '../stately/client.js';
import { camelize } from '../utils.js';

export const platformInfoHandler = asyncHandler(async (req, res) => {
const flavor = (req.query.flavor as string) ?? 'app';

const result = await pool.query<GlobalSettings>({
name: 'get_global_settings',
text: 'SELECT * FROM global_settings where flavor = $1 LIMIT 1',
values: [flavor],
});
const settings =
result.rowCount! > 0
? { ...defaultGlobalSettings, ...camelize(result.rows[0]) }
: defaultGlobalSettings;
let settings: GlobalSettings | undefined = undefined;
try {
// Try StatelyDB first, then fall back to Postgres
const statelySettings = await client.get('GlobalSettings', keyPath`/gs-${flavor}`);
if (statelySettings) {
settings = {
...statelySettings,
// I have to manually convert these to numbers
destinyProfileMinimumRefreshInterval: Number(
statelySettings.destinyProfileMinimumRefreshInterval,
),
destinyProfileRefreshInterval: Number(statelySettings.destinyProfileRefreshInterval),
dimProfileMinimumRefreshInterval: Number(statelySettings.dimProfileMinimumRefreshInterval),
lastUpdated: Number(statelySettings.lastUpdated),
};
}
} catch (e) {
console.error('Error loading global settings from Stately:', flavor, e);
}

if (!settings) {
const result = await pool.query<GlobalSettings>({
name: 'get_global_settings',
text: 'SELECT * FROM global_settings where flavor = $1 LIMIT 1',
values: [flavor],
});
settings =
result.rowCount! > 0
? { ...defaultGlobalSettings, ...camelize(result.rows[0]) }
: defaultGlobalSettings;
}

// Instruct CF to cache for 15 minutes
res.set('Cache-Control', 'public, max-age=900');
Expand Down
20 changes: 18 additions & 2 deletions api/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readFile } from 'fs';
import { sign } from 'jsonwebtoken';
import jwt from 'jsonwebtoken';
import { makeFetch } from 'supertest-fetch';
import { promisify } from 'util';
import { v4 as uuid } from 'uuid';
Expand All @@ -16,6 +16,7 @@ import { Loadout, LoadoutItem } from './shapes/loadouts.js';
import { ProfileResponse, ProfileUpdateRequest, ProfileUpdateResponse } from './shapes/profile.js';
import { SearchType } from './shapes/search.js';
import { defaultSettings } from './shapes/settings.js';
import { client } from './stately/client.js';

const fetch = makeFetch(app);

Expand All @@ -30,11 +31,26 @@ beforeAll(async () => {
expect(testApiKey).toBeDefined();
await refreshApps();

testUserToken = sign({}, process.env.JWT_SECRET!, {
testUserToken = jwt.sign({}, process.env.JWT_SECRET!, {
subject: bungieMembershipId.toString(),
issuer: testApiKey,
expiresIn: 60 * 60,
});

// Make sure we have global settings
const globalSettings = ['dev', 'beta', 'app'].map((stage) =>
client.create('GlobalSettings', {
stage,
dimApiEnabled: true,
destinyProfileMinimumRefreshInterval: 15n,
destinyProfileRefreshInterval: 120n,
autoRefresh: true,
refreshProfileOnVisible: true,
dimProfileMinimumRefreshInterval: 600n,
showIssueBanner: false,
}),
);
await client.putBatch(...globalSettings);
});

afterAll(() => closeDbPool());
Expand Down
2 changes: 1 addition & 1 deletion api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node';
import cors from 'cors';
import express, { ErrorRequestHandler } from 'express';
import { expressjwt as jwt } from 'express-jwt';
import { JwtPayload } from 'jsonwebtoken';
import { type JwtPayload } from 'jsonwebtoken';
import { apiKey, isAppOrigin } from './apps/index.js';
import expressStatsd from './metrics/express.js';
import { metrics } from './metrics/index.js';
Expand Down
3 changes: 3 additions & 0 deletions api/shapes/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface GlobalSettings {
dimProfileMinimumRefreshInterval: number;
/** Display an issue banner, if there is one. */
showIssueBanner: boolean;
/** The unix milliseconds timestamp for when this was last updated. */
lastUpdated: number;
}

export const defaultGlobalSettings: GlobalSettings = {
Expand All @@ -23,6 +25,7 @@ export const defaultGlobalSettings: GlobalSettings = {
refreshProfileOnVisible: true,
dimProfileMinimumRefreshInterval: 600,
showIssueBanner: false,
lastUpdated: 0,
};

export interface PlatformInfoResponse {
Expand Down
6 changes: 6 additions & 0 deletions api/stately/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createClient } from './generated/stately_item_types.js';

/**
* Our StatelyDB client, bound to our types and store.
*/
export const client = createClient(BigInt(process.env.STATELY_STORE_ID!));
4 changes: 4 additions & 0 deletions api/stately/generated/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Code generated by Stately. DO NOT EDIT.

export * from "./stately_pb.js";
export * from "./stately_item_types.js";
4 changes: 4 additions & 0 deletions api/stately/generated/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Code generated by Stately. DO NOT EDIT.

export * from './stately_item_types.js';
export * from './stately_pb.js';
92 changes: 92 additions & 0 deletions api/stately/generated/stately_item_types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Code generated by Stately. DO NOT EDIT.

import type { DatabaseClient as GenericDatabaseClient, StoreID, ClientOptions } from "@stately-cloud/client";
import type {
ApiApp,
GlobalSettings,
ItemAnnotation,
ItemHashTag,
Loadout,
LoadoutShare,
Search,
Settings,
Triumph,
ApiAppSchema,
GlobalSettingsSchema,
ItemAnnotationSchema,
ItemHashTagSchema,
LoadoutSchema,
LoadoutShareSchema,
SearchSchema,
SettingsSchema,
TriumphSchema,
ArtifactUnlocksSchema,
CollapsedSectionSchema,
CustomStatDefSchema,
CustomStatWeightsEntrySchema,
CustomStatsEntrySchema,
InGameLoadoutIdentifiersSchema,
LoadoutItemSchema,
LoadoutParametersSchema,
ModsByBucketEntrySchema,
SocketOverrideSchema,
StatConstraintSchema,
StatConstraintsEntrySchema,
} from "./stately_pb.js";

export declare const itemTypeToSchema: {
"ApiApp": typeof ApiAppSchema,
"GlobalSettings": typeof GlobalSettingsSchema,
"ItemAnnotation": typeof ItemAnnotationSchema,
"ItemHashTag": typeof ItemHashTagSchema,
"Loadout": typeof LoadoutSchema,
"LoadoutShare": typeof LoadoutShareSchema,
"Search": typeof SearchSchema,
"Settings": typeof SettingsSchema,
"Triumph": typeof TriumphSchema,
"ArtifactUnlocks": typeof ArtifactUnlocksSchema,
"CollapsedSection": typeof CollapsedSectionSchema,
"CustomStatDef": typeof CustomStatDefSchema,
"CustomStatWeightsEntry": typeof CustomStatWeightsEntrySchema,
"CustomStatsEntry": typeof CustomStatsEntrySchema,
"InGameLoadoutIdentifiers": typeof InGameLoadoutIdentifiersSchema,
"LoadoutItem": typeof LoadoutItemSchema,
"LoadoutParameters": typeof LoadoutParametersSchema,
"ModsByBucketEntry": typeof ModsByBucketEntrySchema,
"SocketOverride": typeof SocketOverrideSchema,
"StatConstraint": typeof StatConstraintSchema,
"StatConstraintsEntry": typeof StatConstraintsEntrySchema,
};

// AllItemTypes is a convenience type that represents all item type names in your schema.
export type AllItemTypes =
| "ApiApp"
| "GlobalSettings"
| "ItemAnnotation"
| "ItemHashTag"
| "Loadout"
| "LoadoutShare"
| "Search"
| "Settings"
| "Triumph";

// AnyItem is a convenience type that represents any item shape in your schema.
export type AnyItem =
| ApiApp
| GlobalSettings
| ItemAnnotation
| ItemHashTag
| Loadout
| LoadoutShare
| Search
| Settings
| Triumph;

// DatabaseClient is a database client that has been customized with your schema.
export type DatabaseClient = GenericDatabaseClient<typeof itemTypeToSchema, AllItemTypes>;

// createClient creates a new database client with your schema.
export declare function createClient(
storeId: StoreID,
opts?: ClientOptions,
): DatabaseClient;
57 changes: 57 additions & 0 deletions api/stately/generated/stately_item_types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Code generated by Stately. DO NOT EDIT.

import { createClient as createGenericClient } from '@stately-cloud/client';
import {
ApiAppSchema,
ArtifactUnlocksSchema,
CollapsedSectionSchema,
CustomStatDefSchema,
CustomStatWeightsEntrySchema,
CustomStatsEntrySchema,
GlobalSettingsSchema,
InGameLoadoutIdentifiersSchema,
ItemAnnotationSchema,
ItemHashTagSchema,
LoadoutItemSchema,
LoadoutParametersSchema,
LoadoutSchema,
LoadoutShareSchema,
ModsByBucketEntrySchema,
SearchSchema,
SettingsSchema,
SocketOverrideSchema,
StatConstraintSchema,
StatConstraintsEntrySchema,
TriumphSchema,
} from './stately_pb.js';

export const typeToSchema = {
// itemTypes
ApiApp: ApiAppSchema,
GlobalSettings: GlobalSettingsSchema,
ItemAnnotation: ItemAnnotationSchema,
ItemHashTag: ItemHashTagSchema,
Loadout: LoadoutSchema,
LoadoutShare: LoadoutShareSchema,
Search: SearchSchema,
Settings: SettingsSchema,
Triumph: TriumphSchema,

// objectTypes
ArtifactUnlocks: ArtifactUnlocksSchema,
CollapsedSection: CollapsedSectionSchema,
CustomStatDef: CustomStatDefSchema,
CustomStatWeightsEntry: CustomStatWeightsEntrySchema,
CustomStatsEntry: CustomStatsEntrySchema,
InGameLoadoutIdentifiers: InGameLoadoutIdentifiersSchema,
LoadoutItem: LoadoutItemSchema,
LoadoutParameters: LoadoutParametersSchema,
ModsByBucketEntry: ModsByBucketEntrySchema,
SocketOverride: SocketOverrideSchema,
StatConstraint: StatConstraintSchema,
StatConstraintsEntry: StatConstraintsEntrySchema,
};

export function createClient(storeId, opts) {
return createGenericClient(storeId, typeToSchema, opts);
}
Loading

0 comments on commit 84b65b9

Please sign in to comment.