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

Initial Stately schema, generated code, and usage in GlobalSettings #221

Merged
merged 9 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading