More service naming consistency cleanup

This commit is contained in:
Dominic Ferrando
2026-07-04 15:10:06 -04:00
parent f4aad56ad3
commit afdc8de77c
5 changed files with 113 additions and 102 deletions
+23 -22
View File
@@ -22,9 +22,10 @@ import { StandardsParametersSchema } from "@blade-and-brawn/calculator";
// SERVICES
// -----------
const calculatorService = new CalculatorService();
const commerceService = new CommerceService();
const s = {
Calculator: new CalculatorService(),
Commerce: new CommerceService()
};
// ELYSIA
// -----------
@@ -103,7 +104,7 @@ export const app = new Elysia()
(app) => app
// Non-authenticated
.post("/calculate", async ({ body }) => {
return { levels: calculatorService.calculate(body.player, body.activityPerformances) };
return { levels: s.Calculator.calculate(body.player, body.activityPerformances) };
}, {
body: t.Object({
player: PlayerSchema,
@@ -117,23 +118,23 @@ export const app = new Elysia()
return { sessionId: token.sessionId.toString() };
})
.put("/config/:id", async ({ params: { id }, body: { datasetId, parameters } }) => {
await calculatorService.standards.setConfig(id, datasetId, parameters);
await s.Calculator.Standards.Config.update(id, datasetId, parameters);
}, {
params: t.Object({ id: t.String() }),
body: t.Object({ datasetId: t.String(), parameters: StandardsParametersSchema })
})
.post("/config", async ({ body: { datasetId, parameters } }) => {
await calculatorService.standards.createConfig(datasetId, parameters);
await s.Calculator.Standards.Config.create(datasetId, parameters);
}, {
body: t.Object({ datasetId: t.String(), parameters: StandardsParametersSchema })
})
.post("/config/:id/select", async ({ params: { id } }) => {
await calculatorService.standards.selectConfig(id);
.post("/config/:id/switch", async ({ params: { id } }) => {
await s.Calculator.Standards.Config.switch(id);
}, {
params: t.Object({ id: t.String() })
})
.get("/config", async () => {
return await calculatorService.standards.getSelectedConfig();
return await s.Calculator.Standards.Config.get();
})
)
@@ -149,17 +150,17 @@ export const app = new Elysia()
app
// Sync status
.get("/", async ({ sessionId }) => {
const latestSync = await commerceService.getLatestSync(sessionId);
const latestSync = await s.Commerce.Sync.getLatest(sessionId);
if (!latestSync) throw new NotFoundError("No sync found");
return {
startDate: latestSync.started_at,
status: commerceService.deriveSyncStatus(latestSync),
status: s.Commerce.Sync.deriveStatus(latestSync),
syncingPrintfulProductIds: latestSync.syncing_printful_product_ids
}
})
// Run sync
.post("/:printfulProductId?", async ({ params: { printfulProductId }, sessionId }) => {
await commerceService.sync({
await s.Commerce.Sync.start({
session: { id: sessionId, name: "Portal" },
filter: {
printfulProductIds: printfulProductId ? [printfulProductId] : null
@@ -168,13 +169,13 @@ export const app = new Elysia()
}, { params: t.Object({ printfulProductId: t.Optional(t.Numeric()) }) }),
)
.get("/:printfulProductId", async ({ params: { printfulProductId } }) => {
const printfulProduct = await commerceService.printful.Products.get(printfulProductId);
const printfulProduct = await s.Commerce.Printful.Products.get(printfulProductId);
if (!printfulProduct) throw new NotFoundError("Missing printful product");
const webflowProductId = printfulProduct.sync_product.external_id.split("-")[0];
if (!webflowProductId) throw new NotFoundError("Missing webflow product ID");
const webflowProduct = await commerceService.webflow.Products.get(webflowProductId);
const webflowProduct = await s.Commerce.Webflow.Products.get(webflowProductId);
return { printfulProduct, webflowProduct };
}, {
@@ -192,7 +193,7 @@ export const app = new Elysia()
const printfulProduct = payload.data.sync_product;
log.info({ productId: printfulProduct.id }, "printful webhook: product updated");
await commerceService.sync({
await s.Commerce.Sync.start({
session: { id: randomUUIDv7(), name: "Printful" },
filter: {
printfulProductIds: [printfulProduct.id]
@@ -206,7 +207,7 @@ export const app = new Elysia()
log.info({ externalId: payload.data.sync_product.external_id, webflowProductId }, "printful webhook: product deleted");
if (!webflowProductId) throw new NotFoundError("Missing webflow product ID");
await commerceService.webflow.Products.remove(webflowProductId);
await s.Commerce.Webflow.Products.remove(webflowProductId);
break;
}
case Printful.Webhook.Event.PackageShipped: {
@@ -214,12 +215,12 @@ export const app = new Elysia()
const shipInfo = payload.data.shipment;
log.info({ webflowOrderId, carrier: shipInfo.carrier, tracking: shipInfo.tracking_number }, "printful webhook: package shipped");
await commerceService.webflow.Orders.update(webflowOrderId, {
await s.Commerce.Webflow.Orders.update(webflowOrderId, {
shippingTrackingURL: shipInfo.tracking_url,
shippingTracking: shipInfo.tracking_number,
shippingProvider: shipInfo.carrier,
});
await commerceService.webflow.Orders.fulfill(webflowOrderId, {
await s.Commerce.Webflow.Orders.fulfill(webflowOrderId, {
sendOrderFulfilledEmail: true,
});
break;
@@ -229,7 +230,7 @@ export const app = new Elysia()
}
})
.post("/webhook/webflow", async ({ request, body }) => {
if (!commerceService.webflow.Util.verifyWebflowSignature(request, body))
if (!s.Commerce.Webflow.Util.verifyWebflowSignature(request, body))
throw status(400, "Invalid signature");
const payload = body as Webflow.Webhook.EventPayload;
@@ -239,7 +240,7 @@ export const app = new Elysia()
const webflowOrder = payload.payload;
log.info({ orderId: webflowOrder.orderId }, "webflow webhook: order created");
await commerceService.printful.Orders.create({
await s.Commerce.Printful.Orders.create({
external_id: webflowOrder.orderId,
// TODO: derive from webflow
shipping: "STANDARD",
@@ -272,8 +273,8 @@ app.listen(3000, async () => {
log.info({ port: 3000 }, "server started")
try {
const frontQueuedSync = await commerceService.queue.front();
if (frontQueuedSync) await commerceService.sync(frontQueuedSync);
const frontQueuedSync = await s.Commerce.Sync.Queue.front();
if (frontQueuedSync) await s.Commerce.Sync.start(frontQueuedSync);
}
catch (err) {
log.error({ err }, "failed to resume product sync queue")
+42 -33
View File
@@ -5,45 +5,46 @@ import { Value } from "@sinclair/typebox/value";
import { log } from "../util";
export class CalculatorService {
private calculator: LevelCalculator = new LevelCalculator(new Standards(DEFAULT_STANDARDS_DATASET));
standards: StandardsService = new StandardsService(this.calculator);
private Calculator = new LevelCalculator(new Standards(DEFAULT_STANDARDS_DATASET));
Standards: StandardsService = new StandardsService(this.Calculator);
calculate(player: Player, activityPerformances: ActivityPerformance[]) {
this.standards.refreshConfig({ onlyIfStale: true }).catch((err) => log.error({ err }, "failed to refresh standards config"));
return this.calculator.calculate(player, activityPerformances);
this.Standards.Config.refresh({ onlyIfStale: true }).catch((err) => log.error({ err }, "failed to refresh standards config"));
return this.Calculator.calculate(player, activityPerformances);
}
}
class StandardsService {
private static readonly CONFIG_REFRESH_INTERVAL_MS = 30_000;
Config: StandardsConfigService;
constructor(private Calculator: LevelCalculator) {
this.Config = new StandardsConfigService(this.Calculator);
}
}
class StandardsConfigService {
private static readonly REFRESH_INTERVAL_MS = 30_000;
private cachedParametersStringified: string | null = null;
private cachedDatasetId: string | null = null;
private lastRefreshedAt = 0;
constructor(private calculator: LevelCalculator) { }
constructor(private Calculator: LevelCalculator) { }
async getSelectedConfig(opt: { skipCache?: boolean } = {}): Promise<StandardsParameters> {
if (!opt.skipCache) return this.calculator.standards.params;
await this.refreshConfig();
return this.calculator.standards.params;
async create(datasetId: string, parameters: StandardsParameters) {
await db.insertInto("standards_configs")
.values({ parameters, dataset_id: datasetId })
.execute();
}
async selectConfig(id: string) {
await db.transaction().execute(async (trx) => {
await trx.updateTable("standards_configs")
.set({ is_selected: false })
.where("is_selected", "=", true)
.execute();
const result = await trx.updateTable("standards_configs")
.set({ is_selected: true })
.where("id", "=", id)
.execute();
if (result[0]?.numUpdatedRows === 0n) throw new Error(`No config with id "${id}"`);
});
/** Retrieves the currently selected configuration */
async get(opt: { skipCache?: boolean } = {}): Promise<StandardsParameters> {
if (!opt.skipCache) return this.Calculator.Standards.params;
await this.refresh();
return this.Calculator.Standards.params;
}
async setConfig(id: string, datasetId: string, parameters: StandardsParameters) {
async update(id: string, datasetId: string, parameters: StandardsParameters) {
const result = await db.updateTable("standards_configs")
.set({ parameters, dataset_id: datasetId })
.where("id", "=", id)
@@ -51,15 +52,9 @@ class StandardsService {
if (result[0]?.numUpdatedRows === 0n) throw new Error(`No config with id "${id}"`);
}
async createConfig(datasetId: string, parameters: StandardsParameters) {
await db.insertInto("standards_configs")
.values({ parameters, dataset_id: datasetId })
.execute();
}
async refreshConfig(opt: { onlyIfStale?: boolean } = {}) {
async refresh(opt: { onlyIfStale?: boolean } = {}) {
if (opt.onlyIfStale)
if (Date.now() - this.lastRefreshedAt < StandardsService.CONFIG_REFRESH_INTERVAL_MS) return;
if (Date.now() - this.lastRefreshedAt < StandardsConfigService.REFRESH_INTERVAL_MS) return;
this.lastRefreshedAt = Date.now();
@@ -79,8 +74,22 @@ class StandardsService {
const parametersStringified = JSON.stringify(config.parameters);
if (parametersStringified === this.cachedParametersStringified && config.datasetId === this.cachedDatasetId) return;
this.calculator.standards = new Standards(config.dataset, config.parameters);
this.Calculator.Standards = new Standards(config.dataset, config.parameters);
this.cachedParametersStringified = parametersStringified;
this.cachedDatasetId = config.datasetId;
}
async switch(id: string) {
await db.transaction().execute(async (trx) => {
await trx.updateTable("standards_configs")
.set({ is_selected: false })
.where("is_selected", "=", true)
.execute();
const result = await trx.updateTable("standards_configs")
.set({ is_selected: true })
.where("id", "=", id)
.execute();
if (result[0]?.numUpdatedRows === 0n) throw new Error(`No config with id "${id}"`);
});
}
}
+22 -21
View File
@@ -19,32 +19,33 @@ type QueuedSync = {
class AlreadyClaimedError extends Error { }
export class CommerceService {
public readonly printful: PrintfulClient;
public readonly webflow: WebflowClient;
private readonly productSyncer: ProductSyncer;
readonly queue: SyncQueue;
constructor() {
this.printful = new PrintfulClient({
public readonly Printful = new PrintfulClient({
token: env.PRINTFUL_AUTH,
storeId: env.PRINTFUL_STORE_ID,
});
this.webflow = new WebflowClient({
public readonly Webflow = new WebflowClient({
siteId: env.WEBFLOW_SITE_ID,
collectionsId: env.WEBFLOW_COLLECTION_ID,
token: env.WEBFLOW_AUTH,
webhookSecret: env.WEBFLOW_WEBHOOK_SECRET,
});
this.productSyncer = new ProductSyncer(this.printful, this.webflow, log);
this.queue = new SyncQueue();
readonly Sync = new SyncService(this.Printful, this.Webflow);
}
async sync(queuedSync: QueuedSync) {
class SyncService {
readonly Queue = new SyncQueueService();
private readonly ProductSyncer: ProductSyncer;
constructor(private readonly Printful: PrintfulClient, private readonly Webflow: WebflowClient) {
this.ProductSyncer = new ProductSyncer(this.Printful, this.Webflow, log);
}
async start(queuedSync: QueuedSync) {
let id = queuedSync.id ?? null;
let freedSlot = false;
try {
log.info({ session: queuedSync.session }, "starting sync run");
await this.productSyncer.sync({
await this.ProductSyncer.run({
filter: queuedSync.filter,
onStart: async (startDate) => {
const values: Insertable<ProductSyncs> = {
@@ -95,7 +96,7 @@ export class CommerceService {
return;
}
const activeSync = await this.getActiveSync();
const activeSync = await this.getActive();
const activeSyncConflict = activeSync && err instanceof DatabaseError && err.code === "23505";
if (activeSyncConflict) {
@@ -106,11 +107,11 @@ export class CommerceService {
.set({ syncing_printful_product_ids: [], ended_at: new Date(), has_failed: true })
.where("id", "=", activeSync.id)
.execute();
await this.sync(queuedSync);
await this.start(queuedSync);
}
else {
log.info("sync already active, enqueuing next sync");
await this.queue.enqueue(queuedSync);
await this.Queue.enqueue(queuedSync);
}
}
else {
@@ -127,8 +128,8 @@ export class CommerceService {
finally {
if (freedSlot) {
try {
const frontQueuedSync = await this.queue.front();
if (frontQueuedSync) await this.sync(frontQueuedSync);
const frontQueuedSync = await this.Queue.front();
if (frontQueuedSync) await this.start(frontQueuedSync);
}
catch (err) {
log.error({ err }, "failed to process next queued sync")
@@ -137,7 +138,7 @@ export class CommerceService {
}
}
async getActiveSync(sessionId?: string) {
async getActive(sessionId?: string) {
return await db.selectFrom("product_syncs")
.selectAll()
.$if(sessionId !== undefined, (qb) =>
@@ -149,7 +150,7 @@ export class CommerceService {
.executeTakeFirst();
}
async getLatestSync(sessionId?: string) {
async getLatest(sessionId?: string) {
return await db.selectFrom("product_syncs")
.selectAll()
.$if(sessionId !== undefined, (qb) =>
@@ -160,14 +161,14 @@ export class CommerceService {
.executeTakeFirst();
}
deriveSyncStatus(sync: Selectable<ProductSyncs>): SyncStatus {
deriveStatus(sync: Selectable<ProductSyncs>): SyncStatus {
if (!sync.started_at) return "queued";
if (!sync.ended_at) return "active";
return sync.has_failed ? "failed" : "succeeded";
}
}
class SyncQueue {
class SyncQueueService {
async enqueue(queuedSync: QueuedSync) {
const values: Insertable<ProductSyncs> = {
session_id: queuedSync.session.id,
+6 -6
View File
@@ -73,10 +73,10 @@ const metricPriority = (m: NumberMetric) => {
export const DEFAULT_STANDARDS_DATASET = defaultStandardsDataset as StandardsDataset;
export class LevelCalculator {
standards: Standards;
Standards: Standards;
public constructor(standards: Standards) {
this.standards = standards;
this.Standards = standards;
}
// -------------------------------------------------------------------------------------------------
@@ -138,7 +138,7 @@ export class LevelCalculator {
(Object.values(Attribute) as Attribute[]).forEach((attribute) => {
const attrActivityPerformances = activityPerformances.filter(
(p) =>
this.standards.byActivity(p.activity).getMetadata()
this.Standards.byActivity(p.activity).getMetadata()
.attribute === attribute,
);
attrLevels[attribute] = this.calculateAttributeLevel(
@@ -160,7 +160,7 @@ export class LevelCalculator {
if (
activityPerformances.some(
(p) =>
this.standards.byActivity(p.activity).getMetadata()
this.Standards.byActivity(p.activity).getMetadata()
.attribute !== attribute,
)
) {
@@ -168,7 +168,7 @@ export class LevelCalculator {
}
// must perform all of an attributes activities
for (const activity of this.standards.getAttributeActivities(
for (const activity of this.Standards.getAttributeActivities(
attribute,
)) {
const filteredActivites = activityPerformances.map(
@@ -183,7 +183,7 @@ export class LevelCalculator {
}
const activityLevels = activityPerformances.map((p) => {
const interpolatedStandard = this.standards
const interpolatedStandard = this.Standards
.byActivity(p.activity)
.byMetrics(player.metrics)
.getOneInterpolated();
+13 -13
View File
@@ -30,14 +30,14 @@ export class ProductSyncer {
private log: pino.Logger;
constructor(
private printful: PrintfulClient,
private webflow: WebflowClient,
private Printful: PrintfulClient,
private Webflow: WebflowClient,
log?: pino.Logger,
) {
this.log = (log ?? pino({ level: "silent" })).child({ component: "ProductSyncer" });
}
async sync(opt: ProductSyncerOptions = {}) {
async run(opt: ProductSyncerOptions = {}) {
const startDate = new Date();
await opt.onStart?.(startDate);
@@ -47,10 +47,10 @@ export class ProductSyncer {
this.log.info("sync started (all)");
this.log.debug("retrieving webflow products");
const allWebflowProducts = await this.webflow.Products.list({ forceAll: true });
const allWebflowProducts = await this.Webflow.Products.list({ forceAll: true });
this.log.debug("retrieving printful products");
const allPrintfulProducts = await this.printful.Products.list({ forceAll: true });
const allPrintfulProducts = await this.Printful.Products.list({ forceAll: true });
this.log.info({ printfulProductCount: allPrintfulProducts.length, filter: opt.filter ?? "N/A" }, "generating meta printful products");
const metaPrintfulProducts = await this.generateMetaPrintfulProducts(allWebflowProducts, allPrintfulProducts, opt);
@@ -120,7 +120,7 @@ export class ProductSyncer {
this.log.warn({ webflowSku: newWebflowSkus[0] }, "could not find existing webflow SKU that matches");
}
await this.webflow.Products.update(webflowProduct.product.id, {
await this.Webflow.Products.update(webflowProduct.product.id, {
product: { fieldData: webflowProductFieldData },
sku: newWebflowSkus[0],
});
@@ -133,13 +133,13 @@ export class ProductSyncer {
newWebflowSku.id,
);
if (existingWebflowSku) {
await this.webflow.Products.Skus.update(
await this.Webflow.Products.Skus.update(
metaPrintfulProduct.webflowProductId,
existingWebflowSku.id,
newWebflowSku,
);
} else {
await this.webflow.Products.Skus.create(
await this.Webflow.Products.Skus.create(
metaPrintfulProduct.webflowProductId,
[newWebflowSku],
);
@@ -148,18 +148,18 @@ export class ProductSyncer {
} else {
this.log.info("existing webflow product not found, creating it");
const webflowProductId = await this.webflow.Products.create({
const webflowProductId = await this.Webflow.Products.create({
product: { fieldData: webflowProductFieldData },
sku: newWebflowSkus[0],
});
metaPrintfulProduct.webflowProductId = webflowProductId;
if (newWebflowSkus.length > 1)
await this.webflow.Products.Skus.create(webflowProductId, newWebflowSkus.slice(1));
await this.Webflow.Products.Skus.create(webflowProductId, newWebflowSkus.slice(1));
}
// Re-fetch after SKU mutations so newly created SKUs have their real IDs.
const freshWebflowProduct = await this.webflow.Products.get(metaPrintfulProduct.webflowProductId);
const freshWebflowProduct = await this.Webflow.Products.get(metaPrintfulProduct.webflowProductId);
if (!freshWebflowProduct) throw new Error("webflow product missing");
// Update printful external IDs
@@ -186,7 +186,7 @@ export class ProductSyncer {
await sleep(10000);
this.log.info({ printfulProduct: printfulProduct.sync_product.id, externalId: expectedExternalId }, "updating printful product's external ID");
await this.printful.Products.update(printfulProduct.sync_product.id, {
await this.Printful.Products.update(printfulProduct.sync_product.id, {
sync_product: {
id: printfulProduct.sync_product.id,
external_id: expectedExternalId,
@@ -286,7 +286,7 @@ export class ProductSyncer {
metaPrintfulProduct.webflowProductId = webflowProductId;
}
const fullPrintfulProduct = await this.printful.Products.get(printfulProduct.id);
const fullPrintfulProduct = await this.Printful.Products.get(printfulProduct.id);
if (!fullPrintfulProduct) throw new Error(`Printful product ${printfulProduct.id} not found`);
const isDuplicate = allPrintfulProducts.some((p) => p.id !== printfulProduct.id && p.name === printfulProduct.name);