mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-18 20:59:55 -05:00
feat(sync): notify on enable, change defaults to daily and designer profiles
This commit is contained in:
1 parent
acfa844ebe
commit
9334438a52
11 files changed
+60
-15
No files matched your search
@@ -0,0 +1,2 @@
|
||||
-- New syncs are checked daily. Existing syncs keep their interval.
|
||||
ALTER TABLE "CollectionSync" ALTER COLUMN "intervalHours" SET DEFAULT 24;
|
||||
@@ -208,9 +208,10 @@ model CollectionSync {
|
||||
externalId String
|
||||
sourceUrl String
|
||||
// MakerWorld only: which print profiles a new model brings ("url" is the model's default one).
|
||||
// New MakerWorld syncs start on "designer" (see createSync).
|
||||
profileScope String @default("url")
|
||||
// How often the provider's collection is checked: 1, 6 or 24.
|
||||
intervalHours Int @default(1)
|
||||
intervalHours Int @default(24)
|
||||
// Every model id the provider's collection has listed, so a sync only imports what's new since.
|
||||
knownIds String[] @default([])
|
||||
// Checks in a row that found the provider's collection gone. One bad answer doesn't unsync it.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { COLLECTION_SYNC_ENABLED } from "../config";
|
||||
|
||||
/** The intervals a collection can pick, in hours. The shortest is also the default. */
|
||||
/** The intervals a collection can pick, in hours. New syncs get 24 (see CollectionSync.intervalHours). */
|
||||
export const SYNC_INTERVAL_HOURS = [1, 6, 24] as const;
|
||||
export type SyncIntervalHours = (typeof SYNC_INTERVAL_HOURS)[number];
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { parseMakerworldCollectionUrl } from "./makerworldCollections";
|
||||
import { parsePrintablesCollectionUrl } from "./printablesApi";
|
||||
import { parseThingiverseCollectionUrl, parseThingiverseLikesUrl } from "./thingiverseApi";
|
||||
import { createLog } from "./auditLog";
|
||||
import { createNotification } from "./notificationService";
|
||||
import type { SyncIntervalHours } from "./collectionSyncSchedule";
|
||||
import {
|
||||
LIKES_ID_PREFIX,
|
||||
@@ -206,9 +207,38 @@ export async function enableSync(userId: string, input: EnableSyncInput): Promis
|
||||
targetId: created.collectionId,
|
||||
details: { name: created.collection.name, provider: source.provider, url: source.url },
|
||||
});
|
||||
// The sync exists by now, so a failed notification mustn't fail the request: a retry finds the
|
||||
// sync and wouldn't send it either.
|
||||
await notifySyncEnabled(created).catch((err) => console.error("[sync] enable notification failed", err));
|
||||
return toSyncLinkOut(created);
|
||||
}
|
||||
|
||||
const INTERVAL_PHRASES: Record<number, string> = { 1: "every hour", 6: "every 6 hours", 24: "once a day" };
|
||||
const PROFILE_SCOPE_PHRASES: Record<string, string> = {
|
||||
url: "its default print profile",
|
||||
designer: "all of the designer's print profiles",
|
||||
all: "all of its print profiles, community ones included",
|
||||
};
|
||||
|
||||
/** Confirms in the app what was switched on from Thingport Grab, with the defaults it got. */
|
||||
async function notifySyncEnabled(sync: CollectionSync & { collection: Collection }): Promise<void> {
|
||||
const name = sync.collection.name;
|
||||
const interval = INTERVAL_PHRASES[sync.intervalHours] ?? `every ${sync.intervalHours} hours`;
|
||||
const profiles =
|
||||
sync.provider === "makerworld" && PROFILE_SCOPE_PHRASES[sync.profileScope]
|
||||
? `, each with ${PROFILE_SCOPE_PHRASES[sync.profileScope]}`
|
||||
: "";
|
||||
await createNotification(sync.userId, {
|
||||
type: "sync",
|
||||
title: `"${name}" is now synced`,
|
||||
body:
|
||||
`New models in ${sourceDescription(sync)} are imported here ${interval}${profiles}. ` +
|
||||
"Change this in the collection's sync configuration.",
|
||||
externalUrl: sync.sourceUrl,
|
||||
internalPath: `/models/collections/${sync.collectionId}`,
|
||||
});
|
||||
}
|
||||
|
||||
// How many "Things (MakerWorld 2)"-style names to try before giving up.
|
||||
const MAX_NAME_SUFFIX = 20;
|
||||
|
||||
@@ -222,6 +252,9 @@ function* collectionNamesFor(title: string, provider: string): Generator<string>
|
||||
for (let n = 2; n <= MAX_NAME_SUFFIX; n++) yield `${title} (${label} ${n})`;
|
||||
}
|
||||
|
||||
// A synced collection follows what the designer publishes, so new models bring all of their profiles.
|
||||
const NEW_MAKERWORLD_SYNC_PROFILE_SCOPE: SyncProfileScope = "designer";
|
||||
|
||||
/** Links `collection` to `source`. `takenBy` names the provider when the collection already follows
|
||||
* another source; `link` is the existing one when another request just made this same link. */
|
||||
async function createSync(
|
||||
@@ -241,6 +274,7 @@ async function createSync(
|
||||
externalId: source.externalId,
|
||||
sourceUrl: source.url,
|
||||
knownIds,
|
||||
...(source.provider === "makerworld" ? { profileScope: NEW_MAKERWORLD_SYNC_PROFILE_SCOPE } : {}),
|
||||
},
|
||||
include: { collection: true },
|
||||
});
|
||||
|
||||
@@ -120,7 +120,7 @@ async function importDirectly(designId: string) {
|
||||
}
|
||||
|
||||
async function latestNotification() {
|
||||
return prisma.notification.findFirstOrThrow({ where: { userId }, orderBy: { createdAt: "desc" } });
|
||||
return prisma.notification.findFirstOrThrow({ where: { userId }, orderBy: [{ createdAt: "desc" }, { id: "desc" }] });
|
||||
}
|
||||
|
||||
describe("synced collections", () => {
|
||||
@@ -164,7 +164,12 @@ describe("synced collections", () => {
|
||||
|
||||
const list = await request(app).get("/api/collections").set(auth());
|
||||
const listed = list.body.find((c: { id: string }) => c.id === res.body.collection_id);
|
||||
expect(listed.sync).toMatchObject({ provider: "makerworld", profile_scope: "url" });
|
||||
expect(listed.sync).toMatchObject({ provider: "makerworld", profile_scope: "designer", interval_hours: 24 });
|
||||
|
||||
const notification = await latestNotification();
|
||||
expect(notification.title).toBe(`"Phoenix flock" is now synced`);
|
||||
expect(notification.body).toContain("once a day, each with all of the designer's print profiles");
|
||||
expect(notification.internalPath).toBe(`/models/collections/${res.body.collection_id}`);
|
||||
});
|
||||
|
||||
it("brings back a deleted collection of already-imported models, in the provider's order", async () => {
|
||||
@@ -556,13 +561,13 @@ describe("synced collections", () => {
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("checks every collection on its own interval, 1 hour unless it picks 6 or 24", async () => {
|
||||
it("checks every collection on its own interval, 24 hours unless it picks 1 or 6", async () => {
|
||||
const enabled = await enable({
|
||||
url: `https://www.thingiverse.com/collections/${newId()}`,
|
||||
title: `Thingiverse ${newId()}`,
|
||||
known_ids: [],
|
||||
});
|
||||
expect(enabled.body.interval_hours).toBe(1);
|
||||
expect(enabled.body.interval_hours).toBe(24);
|
||||
const patch = (body: Record<string, unknown>) =>
|
||||
request(app).patch(`/api/collection/${enabled.body.collection_id}/sync`).set(auth()).send(body);
|
||||
|
||||
|
||||
Reference in new issue
Block a user