From ef0020cdd725b884bd7969dc94577949d0095953 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 6 Apr 2024 16:18:00 -0500 Subject: [PATCH] Add view counter --- apps/timsexperiments/.env.local | 3 +- apps/timsexperiments/package.json | 2 +- .../src/components/nav/Nav.astro | 2 +- apps/timsexperiments/src/env.d.ts | 8 +++ .../src/{components/nav => scripts}/nav.ts | 5 -- apps/view-counter/src/index.ts | 4 +- apps/view-counter/src/views/index.ts | 12 +++- packages/view-storage/package.json | 8 +-- packages/view-storage/src/client.ts | 6 +- packages/view-storage/src/schema.ts | 1 + packages/views-client/package.json | 3 + packages/views-client/src/index.ts | 63 +++++++++++++++---- pnpm-lock.yaml | 18 ++++-- 13 files changed, 97 insertions(+), 38 deletions(-) rename apps/timsexperiments/src/{components/nav => scripts}/nav.ts (92%) diff --git a/apps/timsexperiments/.env.local b/apps/timsexperiments/.env.local index 9f02cd7..553fcdf 100644 --- a/apps/timsexperiments/.env.local +++ b/apps/timsexperiments/.env.local @@ -1 +1,2 @@ -PUBLIC_VIEW_COUNTER_API=http://localhost:8787/views \ No newline at end of file +# PUBLIC_VIEW_COUNTER_API=http://localhost:8787/views +PUBLIC_VIEW_COUNTER_API=https://dev.views.timsexperiments.foo/ \ No newline at end of file diff --git a/apps/timsexperiments/package.json b/apps/timsexperiments/package.json index f97a3df..4fbb2d4 100644 --- a/apps/timsexperiments/package.json +++ b/apps/timsexperiments/package.json @@ -27,7 +27,7 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", "@timsexperiments/theme": "^0.0.0", - "@timsexperiments/three-rubiks-cube": "^0.0.3", + "@timsexperiments/three-rubiks-cube": "^0.0.5", "@timsexperiments/views-client": "workspace:*", "@types/hast": "^3.0.4", "@types/react": "^18.2.47", diff --git a/apps/timsexperiments/src/components/nav/Nav.astro b/apps/timsexperiments/src/components/nav/Nav.astro index f597c06..2660add 100644 --- a/apps/timsexperiments/src/components/nav/Nav.astro +++ b/apps/timsexperiments/src/components/nav/Nav.astro @@ -51,4 +51,4 @@ import { Home, Compass, FlaskConical, Mail, UserRoundPlus } from 'lucide-react'; } - + diff --git a/apps/timsexperiments/src/env.d.ts b/apps/timsexperiments/src/env.d.ts index 99a3b5c..d2bfbed 100644 --- a/apps/timsexperiments/src/env.d.ts +++ b/apps/timsexperiments/src/env.d.ts @@ -1,3 +1,11 @@ /// /// /// + +interface ImportMetaEnv { + readonly PUBLIC_VIEW_COUNTER_API: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/apps/timsexperiments/src/components/nav/nav.ts b/apps/timsexperiments/src/scripts/nav.ts similarity index 92% rename from apps/timsexperiments/src/components/nav/nav.ts rename to apps/timsexperiments/src/scripts/nav.ts index 253deb0..56ae5c5 100644 --- a/apps/timsexperiments/src/components/nav/nav.ts +++ b/apps/timsexperiments/src/scripts/nav.ts @@ -36,11 +36,6 @@ const observer = new IntersectionObserver( if (entry.isIntersecting && !anyIntersecting) { anyIntersecting = true; navigator.classList.add('active'); - console.log( - 'scrolling the navigator for', - entry.target.id, - 'into view' - ); navigator.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } else { navigator.classList.remove('active'); diff --git a/apps/view-counter/src/index.ts b/apps/view-counter/src/index.ts index 7bc0d82..9874429 100644 --- a/apps/view-counter/src/index.ts +++ b/apps/view-counter/src/index.ts @@ -4,9 +4,9 @@ import { ViewsHandler } from './views'; /** * Welcome to Cloudflare Workers! This is your first worker. * - * - Run `bun run dev` in your terminal to start a development server + * - Run `pnpm run dev` in your terminal to start a development server * - Open a browser tab at http://localhost:8787/ to see your worker in action - * - Run `bun run deploy` to publish your worker + * - Run `pnpm run deploy` to publish your worker * * Learn more at https://developers.cloudflare.com/workers/ */ diff --git a/apps/view-counter/src/views/index.ts b/apps/view-counter/src/views/index.ts index a87f737..9f534d5 100644 --- a/apps/view-counter/src/views/index.ts +++ b/apps/view-counter/src/views/index.ts @@ -12,7 +12,6 @@ export class ViewsHandler implements RouteHandler { env: Env, baseHeaders: Record, ) { - console.log(env.TURSO_AUTH_TOKEN); const db = createDb({ url: env.TURSO_URL, authToken: env.TURSO_AUTH_TOKEN, @@ -66,8 +65,15 @@ export class ViewsHandler implements RouteHandler { private async POST(): Promise { const clientIP = this.request.headers.get('CF-Connecting-IP')!; - const view = (await this.request.json()) as Omit; - await this.db.add({ ipAddress: clientIP, ...view }); + const view = (await this.request.json()) as Omit; + const { page, ...rest } = view; + let pageUrl: URL; + try { + pageUrl = new URL(page); + } catch { + return badRequestResponse({ message: 'Page must be a valid fully qualified URL.', headers: this.baseHeaders }); + } + await this.db.add({ ipAddress: clientIP, ...rest, page: pageUrl.href, path: pageUrl.pathname }); return responseNoContent({ headers: this.baseHeaders }); } } diff --git a/packages/view-storage/package.json b/packages/view-storage/package.json index c802ecd..18a8d8d 100644 --- a/packages/view-storage/package.json +++ b/packages/view-storage/package.json @@ -3,6 +3,10 @@ "module": "src/index.ts", "types": "src/index.ts", "type": "module", + "dependencies": { + "@libsql/client": "^0.6.0", + "drizzle-orm": "^0.30.8" + }, "devDependencies": { "@types/bun": "latest", "dotenv": "^16.4.5", @@ -10,9 +14,5 @@ }, "peerDependencies": { "typescript": "^5.0.0" - }, - "dependencies": { - "@libsql/client": "^0.6.0", - "drizzle-orm": "^0.30.8" } } diff --git a/packages/view-storage/src/client.ts b/packages/view-storage/src/client.ts index 666dab6..3ed9420 100644 --- a/packages/view-storage/src/client.ts +++ b/packages/view-storage/src/client.ts @@ -2,14 +2,14 @@ import { createClient } from "@libsql/client"; import { drizzle } from "drizzle-orm/libsql"; export type CreateDbOptions = { + authToken: string; url: string; - authToken?: string; }; -export function createDb({ url, authToken }: CreateDbOptions) { +export function createDb({ authToken, url }: CreateDbOptions) { const client = createClient({ - url, authToken, + url, }); return drizzle(client); } diff --git a/packages/view-storage/src/schema.ts b/packages/view-storage/src/schema.ts index 85bdd72..76fd266 100644 --- a/packages/view-storage/src/schema.ts +++ b/packages/view-storage/src/schema.ts @@ -13,6 +13,7 @@ export const views = sqliteTable( { id: integer("id").primaryKey(), page: text("page", { length: 256 }).notNull(), + path: text("path", { length: 256 }).notNull(), ipAddress: text("ip_address", { length: 40 }).notNull(), viewedAt: integer("viewed_at", { mode: "timestamp_ms" }) .default(sql`CURRENT_TIMESTAMP`) diff --git a/packages/views-client/package.json b/packages/views-client/package.json index 0f81d77..547895c 100644 --- a/packages/views-client/package.json +++ b/packages/views-client/package.json @@ -9,5 +9,8 @@ }, "peerDependencies": { "typescript": "^5.0.0" + }, + "dependencies": { + "valibot": "^0.32.0" } } diff --git a/packages/views-client/src/index.ts b/packages/views-client/src/index.ts index 3d93102..0489c4c 100644 --- a/packages/views-client/src/index.ts +++ b/packages/views-client/src/index.ts @@ -1,16 +1,23 @@ import { type View } from "@timsexperiments/view-storage"; +import * as v from "valibot"; -export type GetViewsOptions = { - page: string; -}; +const GetViewsSchema = v.object({ + page: v.pipe(v.string(), v.url()), +}); -export type AddViewOpitons = { - page: string; -}; +export type GetViewsOptions = v.InferOutput; -export type ViewsClientOptions = { - host: string; -}; +const AddViewSchema = v.object({ + page: v.pipe(v.string(), v.url()), +}); + +export type AddViewOpitons = v.InferOutput; + +const ViewsClientOptionsSchema = v.object({ + host: v.pipe(v.string(), v.url()), +}); + +export type ViewsClientOptions = v.InferOutput; /** * Client for interacting with the timsexperiments views api. @@ -18,7 +25,15 @@ export type ViewsClientOptions = { export class ViewsClient { private readonly url; - constructor({ host }: ViewsClientOptions) { + constructor(options: ViewsClientOptions) { + const parsed = v.safeParse(ViewsClientOptionsSchema, options); + if (!parsed.success) { + throw new ViewClientError( + "Invalid options provided: " + JSON.stringify(parsed.issues) + ); + } + + const { host } = parsed.output; this.url = new URL(host); } @@ -29,7 +44,14 @@ export class ViewsClient { * @param {string} options.page - The page for which to retrieve views. * @returns {Promise} - A promise that resolves to the retrieved views. */ - async getViews({ page }: GetViewsOptions) { + async getViews(options: GetViewsOptions) { + const parsed = v.safeParse(GetViewsSchema, options); + if (!parsed.success) { + throw new ViewClientError( + "Invalid options provided: " + JSON.stringify(parsed.issues) + ); + } + const { page } = parsed.output; const url = this.viewsUrl; url.searchParams.append("page", page); const response = await fetch(url.href); @@ -43,12 +65,20 @@ export class ViewsClient { * @param {string} options.page - The page for which to add a view. * @returns {Promise} - A promise that resolves when the view is added successfully. */ - async addView({ page }: AddViewOpitons) { + async addView(options: AddViewOpitons) { + const parsed = v.safeParse(AddViewSchema, options); + if (!parsed.success) { + throw new ViewClientError( + "Invalid options provided: " + JSON.stringify(parsed.issues) + ); + } + const { page } = parsed.output; + const pageUrl = new URL(page); const url = this.viewsUrl; await fetch(url.href, { method: "POST", body: JSON.stringify({ - page, + page: pageUrl.href, }), headers: { "Content-Type": "application/json", @@ -64,3 +94,10 @@ export class ViewsClient { } export default ViewsClient; + +export class ViewClientError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "ViewClientError"; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a71b91..d6866c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,8 +73,8 @@ importers: specifier: ^0.0.0 version: 0.0.0 '@timsexperiments/three-rubiks-cube': - specifier: ^0.0.3 - version: 0.0.3(three-mesh-bvh@0.7.5(three@0.165.0)) + specifier: ^0.0.5 + version: 0.0.5(three-mesh-bvh@0.7.5(three@0.165.0)) '@timsexperiments/views-client': specifier: workspace:* version: link:../../packages/views-client @@ -219,6 +219,9 @@ importers: typescript: specifier: ^5.0.0 version: 5.4.5 + valibot: + specifier: ^0.32.0 + version: 0.32.0 devDependencies: '@timsexperiments/view-storage': specifier: workspace:* @@ -1810,8 +1813,8 @@ packages: resolution: {integrity: sha512-UdwWfGJPb/5LFIM+g8DNw7qvxz2pI5chexvA6dpJYTBkOUA07yALC++/y6Oz9lxNgIhAAzpbX+6vrsNKleXsQA==} engines: {vscode: ^1.88.0} - '@timsexperiments/three-rubiks-cube@0.0.3': - resolution: {integrity: sha512-LeRPFyOoFEtO/YlpeuvR6yo5vHj0zFJi3y8Xa0gd4T8Bt2fd3h30xDVKBMakCGeWZKAOUsYfhIyoPd6ghQg6mw==} + '@timsexperiments/three-rubiks-cube@0.0.5': + resolution: {integrity: sha512-6rMOe7xA8x1Wuk4gViWjGTFAp1SLTVNv9C7fe9sASXe7E1l8g/TYx3VcM+0vz3J0oEszmlgZ5EEnbH+fC2VwXQ==} '@timsexperiments/ts-plugin-workers-wasm@0.0.2': resolution: {integrity: sha512-REgfwnM/Cgc4AdBszcUOB5et2nDmCiyUeyUZx0Dw/aSvANeZgreRxl+1YfXO1+DPonCmJi4GXOdtE/BEkidKGw==} @@ -4147,6 +4150,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + valibot@0.32.0: + resolution: {integrity: sha512-FXBnJl4bNOmeg7lQv+jfvo/wADsRBN8e9C3r+O77Re3dEnDma8opp7p4hcIbF7XJJ30h/5SVohdjer17/sHOsQ==} + vfile-location@5.0.2: resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==} @@ -5740,7 +5746,7 @@ snapshots: '@timsexperiments/theme@0.0.0': {} - '@timsexperiments/three-rubiks-cube@0.0.3(three-mesh-bvh@0.7.5(three@0.165.0))': + '@timsexperiments/three-rubiks-cube@0.0.5(three-mesh-bvh@0.7.5(three@0.165.0))': dependencies: three: 0.165.0 three-bvh-csg: 0.0.16(three-mesh-bvh@0.7.5(three@0.165.0))(three@0.165.0) @@ -8559,6 +8565,8 @@ snapshots: util-deprecate@1.0.2: {} + valibot@0.32.0: {} + vfile-location@5.0.2: dependencies: '@types/unist': 3.0.2