Skip to content

Commit

Permalink
feat: Add Timescale for Usage tables
Browse files Browse the repository at this point in the history
  • Loading branch information
flemzord committed May 6, 2024
1 parent ac91b9e commit 8ba2140
Show file tree
Hide file tree
Showing 12 changed files with 102 additions and 92 deletions.
4 changes: 4 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@
"@tailwindcss/forms": "^0.5.7",
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"chart.js": "^4.4.2",
"chartjs-adapter-moment": "^1.0.1",
"drizzle-orm": "^0.30.9",
"h3-zod": "^0.5.3",
"moment": "^2.30.1",
"nuxt": "^3.11.2",
"nuxt-auth-utils": "^0.0.24",
"nuxt-cloudflare-analytics": "^1.0.8",
"superjson": "^2.2.1",
"trpc-nuxt": "^0.10.21",
"vue": "^3.4.26",
"vue-chartjs": "^5.3.1",
"vue-router": "^4.3.0",
"zod": "^3.23.5"
},
Expand Down
10 changes: 8 additions & 2 deletions apps/web/server/api/license/validate.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { zh } from 'h3-zod';
import { z } from 'zod';
import {
type License,
checkLicenseIsValid,
writeExpiredLicenseUsage,
writeSuccessLicenseUsage,
} from '~/server/utils/license';

Expand Down Expand Up @@ -51,7 +51,13 @@ export default defineEventHandler(async (event) => {
});
}

await checkLicenseIsValid(license[0]);
if (license[0].expirationDate <= new Date()) {
await writeExpiredLicenseUsage(license[0]);
throw createError({
statusCode: 400,
statusMessage: 'License expired',
});
}
await writeSuccessLicenseUsage(license[0]);

return {
Expand Down
2 changes: 2 additions & 0 deletions apps/web/server/trpc/routers/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { customerRouter } from '~/server/trpc/routers/customer';
import { licenseRouter } from '~/server/trpc/routers/license';
import { licenseUsageRouter } from '~/server/trpc/routers/licenseUsage';
import { productRouter } from '~/server/trpc/routers/product';
import { router, t } from '~/server/trpc/trpc';

export const appRouter = router({
license: licenseRouter,
product: productRouter,
customer: customerRouter,
licenseUsage: licenseUsageRouter,
});

export type AppRouter = typeof appRouter;
19 changes: 19 additions & 0 deletions apps/web/server/trpc/routers/licenseUsage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { schema } from '@getlicensed/db';
import {and, desc, eq, sql} from 'drizzle-orm';
import { z } from 'zod';
import { protectedProcedure, t } from '~/server/trpc/trpc';
import { generateLicenseKey } from '~/server/utils/license';

const GetByIdShape = z.object({
id: z.string(),
});

export const licenseUsageRouter = t.router({
getById: protectedProcedure.input(GetByIdShape).query(({ input, ctx }) => {
return useDB().execute(sql`SELECT time_bucket('5 minutes', created_at) AS time, count(action), action
FROM "License_usage"
WHERE license_id = ${input.id} AND type = 'LICENSE_VALIDATE'
GROUP BY time,action
ORDER BY time DESC LIMIT 10;`)
}),
});
41 changes: 1 addition & 40 deletions apps/web/server/utils/license.test.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,6 @@
// @vitest-environment nuxt
import { expect, test } from 'vitest';
import {
type License,
checkLicenseIsValid,
createRandomString,
} from '~/server/utils/license';

test('check if license is expired', async () => {
const today = new Date();
const dateOffset = 24 * 60 * 60 * 1000 * 5; //5 days

const license: License = {
name: 'test',
token: 'test',
productName: 'test',
customerName: 'test',
expirationDate: new Date(today.setTime(today.getTime() - dateOffset)),
createdAt: today,
updatedAt: today,
};
await expect(checkLicenseIsValid(license)).rejects.toThrowError(
'License expired',
);
});

test('check if license is valid', async () => {
const today = new Date();
const dateOffset = 24 * 60 * 60 * 1000 * 5; //5 days

const license: License = {
name: 'test',
token: 'test',
productName: 'test',
customerName: 'test',
expirationDate: new Date(today.setTime(today.getTime() + dateOffset)),
createdAt: today,
updatedAt: today,
};
const result = await checkLicenseIsValid(license);
expect(result).toEqual(license);
});
import { createRandomString } from '~/server/utils/license';

test('check if random string is generated correctly', () => {
const length = 10;
Expand Down
15 changes: 2 additions & 13 deletions apps/web/server/utils/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,18 @@ export interface License {
updatedAt: Date;
}

export const checkLicenseIsValid = async (license: License) => {
if (license.expirationDate <= new Date()) {
await writeExpiredLicenseUsage(license);
throw createError({
statusCode: 400,
statusMessage: 'License expired',
});
}
return license;
};

export const writeSuccessLicenseUsage = async (license: License) => {
return useDB().insert(schema.licenseUsage).values({
licenseId: license.id,
type: 'LICENSE_VALIDATE',
action: 'SUCCESS',
action: schema.licenseUsageActionEnum.enumValues[0],
});
};

export const writeExpiredLicenseUsage = async (license: License) => {
return useDB().insert(schema.licenseUsage).values({
licenseId: license.id,
type: 'LICENSE_VALIDATE',
action: 'EXPIRED',
action: schema.licenseUsageActionEnum.enumValues[1],
});
};
5 changes: 2 additions & 3 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ volumes:
driver: local
services:
postgres:
image: "postgres:16-alpine"
image: "timescale/timescaledb-ha:pg16"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
Expand All @@ -16,9 +16,8 @@ services:
POSTGRES_USER: "app"
POSTGRES_PASSWORD: "app"
POSTGRES_DB: "app"
PGDATA: /data/postgres
volumes:
- data-postgres:/data/postgres
- data-postgres:/home/postgres/pgdata/data

mailpit:
image: "axllent/mailpit"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,14 @@ EXCEPTION
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "License_usage" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"created_at" timestamp (2) with time zone DEFAULT now() NOT NULL,
"license_id" uuid NOT NULL,
"type" "license_log_type",
"action" "license_log_action",
"metadata" jsonb DEFAULT '{}'::jsonb,
"created_at" timestamp DEFAULT now() NOT NULL
"action" "license_log_action"
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "License_usage" ADD CONSTRAINT "License_usage_license_id_License_id_fk" FOREIGN KEY ("license_id") REFERENCES "License"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
CREATE EXTENSION IF NOT EXISTS timescaledb;
END $$;
--> statement-breakpoint
SELECT create_hypertable('"License_usage"', 'created_at', if_not_exists => TRUE, migrate_data => TRUE);
26 changes: 6 additions & 20 deletions internal/db/src/migrations/meta/0002_snapshot.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"id": "44e6f9c9-26f2-4550-ab91-d58b7e86eb5d",
"id": "c9489e23-7df8-453e-8d60-ca510f1dc969",
"prevId": "e7c6b701-a6f1-4473-b140-98c3349ca2f8",
"version": "5",
"dialect": "pg",
Expand Down Expand Up @@ -186,12 +186,12 @@
"name": "License_usage",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"created_at": {
"name": "created_at",
"type": "timestamp (2) with time zone",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
"default": "now()"
},
"license_id": {
"name": "license_id",
Expand All @@ -210,20 +210,6 @@
"type": "license_log_action",
"primaryKey": false,
"notNull": false
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"primaryKey": false,
"notNull": false,
"default": "'{}'::jsonb"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
Expand Down
4 changes: 2 additions & 2 deletions internal/db/src/migrations/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
{
"idx": 2,
"version": "5",
"when": 1714851280261,
"tag": "0002_milky_big_bertha",
"when": 1714938495594,
"tag": "0002_numerous_marrow",
"breakpoints": true
}
]
Expand Down
9 changes: 4 additions & 5 deletions internal/db/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
pgTable,
text,
timestamp,
unique,
uuid,
} from 'drizzle-orm/pg-core';

Expand Down Expand Up @@ -76,16 +77,14 @@ export const licenseUsageActionEnum = pgEnum('license_log_action', [
]);

export const licenseUsage = pgTable('License_usage', {
id: uuid('id').primaryKey().default(sql`gen_random_uuid()`),
createdAt: timestamp('created_at', { precision: 2, withTimezone: true })
.notNull()
.defaultNow(),
licenseId: uuid('license_id')
.notNull()
.references(() => license.id),
type: licenseUsageTypeEnum('type'),
action: licenseUsageActionEnum('action'),
metadata: jsonb('metadata').default(sql`'{}'::jsonb`),
createdAt: timestamp('created_at', { withTimezone: false })
.notNull()
.defaultNow(),
});

export const licenseUsageRelations = relations(licenseUsage, ({ one }) => ({
Expand Down
47 changes: 47 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 8ba2140

Please sign in to comment.