mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
⭐ feat(api): Add payments with Polar.sh (#264)
## Description <!-- Briefly describe the purpose and scope of your changes --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new subscription API endpoint for managing subscriptions and products. - Enhanced subscription management with new entities and functionalities. - Added functionality to retrieve current timestamps in both local and UTC formats. - Added Polar.sh integration with customer portal and checkout session creation APIs. - **Refactor** - Redesigned team details to now present members and subscription information instead of a plan type. - Enhanced member management by incorporating role assignments. - Streamlined user data handling and removed legacy subscription event logic. - Simplified error handling in actor functions for better clarity. - Updated plan types and UI labels to reflect new subscription tiers. - Improved database indexing for Steam user data. - **Chores** - Updated the database schema with new tables and fields to support subscription, team, and member enhancements. - Extended identifier prefixes to broaden system integration. - Added new secrets related to pricing plans in infrastructure configuration. - Configured API and auth routing with new domain and routing rules. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
25
infra/api.ts
25
infra/api.ts
@@ -6,15 +6,21 @@ import { cluster } from "./cluster";
|
|||||||
import { postgres } from "./postgres";
|
import { postgres } from "./postgres";
|
||||||
|
|
||||||
export const api = new sst.aws.Service("Api", {
|
export const api = new sst.aws.Service("Api", {
|
||||||
|
cluster,
|
||||||
cpu: $app.stage === "production" ? "2 vCPU" : undefined,
|
cpu: $app.stage === "production" ? "2 vCPU" : undefined,
|
||||||
memory: $app.stage === "production" ? "4 GB" : undefined,
|
memory: $app.stage === "production" ? "4 GB" : undefined,
|
||||||
cluster,
|
|
||||||
command: ["bun", "run", "./src/api/index.ts"],
|
command: ["bun", "run", "./src/api/index.ts"],
|
||||||
link: [
|
link: [
|
||||||
bus,
|
bus,
|
||||||
auth,
|
auth,
|
||||||
postgres,
|
postgres,
|
||||||
secret.PolarSecret,
|
secret.PolarSecret,
|
||||||
|
secret.PolarWebhookSecret,
|
||||||
|
secret.NestriFamilyMonthly,
|
||||||
|
secret.NestriFamilyYearly,
|
||||||
|
secret.NestriFreeMonthly,
|
||||||
|
secret.NestriProMonthly,
|
||||||
|
secret.NestriProYearly,
|
||||||
],
|
],
|
||||||
image: {
|
image: {
|
||||||
dockerfile: "packages/functions/Containerfile",
|
dockerfile: "packages/functions/Containerfile",
|
||||||
@@ -23,16 +29,11 @@ export const api = new sst.aws.Service("Api", {
|
|||||||
NO_COLOR: "1",
|
NO_COLOR: "1",
|
||||||
},
|
},
|
||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
domain: "api." + domain,
|
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
listen: "80/http",
|
listen: "80/http",
|
||||||
forward: "3001/http",
|
forward: "3001/http",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
listen: "443/https",
|
|
||||||
forward: "3001/http",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
dev: {
|
dev: {
|
||||||
@@ -48,3 +49,15 @@ export const api = new sst.aws.Service("Api", {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const apiRoute = new sst.aws.Router("ApiRoute", {
|
||||||
|
routes: {
|
||||||
|
// I think api.url should work all the same
|
||||||
|
"/*": api.nodes.loadBalancer.dnsName,
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
name: "api." + domain,
|
||||||
|
dns: sst.cloudflare.dns(),
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,26 +1,14 @@
|
|||||||
import { bus } from "./bus";
|
import { bus } from "./bus";
|
||||||
import { domain } from "./dns";
|
import { domain } from "./dns";
|
||||||
// import { email } from "./email";
|
|
||||||
import { secret } from "./secret";
|
import { secret } from "./secret";
|
||||||
import { postgres } from "./postgres";
|
|
||||||
import { cluster } from "./cluster";
|
import { cluster } from "./cluster";
|
||||||
// sst.Linkable.wrap(random.RandomString, (resource) => ({
|
import { postgres } from "./postgres";
|
||||||
// properties: {
|
|
||||||
// value: resource.result,
|
|
||||||
// },
|
|
||||||
// }));
|
|
||||||
|
|
||||||
// export const authFingerprintKey = new random.RandomString(
|
|
||||||
// "AuthFingerprintKey",
|
|
||||||
// {
|
|
||||||
// length: 32,
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
|
|
||||||
|
//FIXME: Use a shared /tmp folder
|
||||||
export const auth = new sst.aws.Service("Auth", {
|
export const auth = new sst.aws.Service("Auth", {
|
||||||
|
cluster,
|
||||||
cpu: $app.stage === "production" ? "1 vCPU" : undefined,
|
cpu: $app.stage === "production" ? "1 vCPU" : undefined,
|
||||||
memory: $app.stage === "production" ? "2 GB" : undefined,
|
memory: $app.stage === "production" ? "2 GB" : undefined,
|
||||||
cluster,
|
|
||||||
command: ["bun", "run", "./src/auth.ts"],
|
command: ["bun", "run", "./src/auth.ts"],
|
||||||
link: [
|
link: [
|
||||||
bus,
|
bus,
|
||||||
@@ -38,18 +26,12 @@ export const auth = new sst.aws.Service("Auth", {
|
|||||||
NO_COLOR: "1",
|
NO_COLOR: "1",
|
||||||
STORAGE: $dev ? "/tmp/persist.json" : "/mnt/efs/persist.json"
|
STORAGE: $dev ? "/tmp/persist.json" : "/mnt/efs/persist.json"
|
||||||
},
|
},
|
||||||
//TODO: Use API gateway instead, because of the API headers
|
|
||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
domain: "auth." + domain,
|
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
listen: "80/http",
|
listen: "80/http",
|
||||||
forward: "3002/http",
|
forward: "3002/http",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
listen: "443/https",
|
|
||||||
forward: "3002/http",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
permissions: [
|
permissions: [
|
||||||
@@ -71,3 +53,14 @@ export const auth = new sst.aws.Service("Auth", {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const authRoute = new sst.aws.Router("AuthRoute", {
|
||||||
|
routes: {
|
||||||
|
// I think auth.url should work all the same
|
||||||
|
"/*": auth.nodes.loadBalancer.dnsName,
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
name: "auth." + domain,
|
||||||
|
dns: sst.cloudflare.dns(),
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
export const secret = {
|
export const secret = {
|
||||||
// InstantAppId: new sst.Secret("InstantAppId"),
|
|
||||||
PolarSecret: new sst.Secret("PolarSecret", process.env.POLAR_API_KEY),
|
PolarSecret: new sst.Secret("PolarSecret", process.env.POLAR_API_KEY),
|
||||||
GithubClientID: new sst.Secret("GithubClientID"),
|
GithubClientID: new sst.Secret("GithubClientID"),
|
||||||
DiscordClientID: new sst.Secret("DiscordClientID"),
|
DiscordClientID: new sst.Secret("DiscordClientID"),
|
||||||
|
PolarWebhookSecret: new sst.Secret("PolarWebhookSecret"),
|
||||||
GithubClientSecret: new sst.Secret("GithubClientSecret"),
|
GithubClientSecret: new sst.Secret("GithubClientSecret"),
|
||||||
// InstantAdminToken: new sst.Secret("InstantAdminToken"),
|
|
||||||
DiscordClientSecret: new sst.Secret("DiscordClientSecret"),
|
DiscordClientSecret: new sst.Secret("DiscordClientSecret"),
|
||||||
|
|
||||||
|
// Pricing
|
||||||
|
NestriFreeMonthly: new sst.Secret("NestriFreeMonthly"),
|
||||||
|
NestriProMonthly: new sst.Secret("NestriProMonthly"),
|
||||||
|
NestriProYearly: new sst.Secret("NestriProYearly"),
|
||||||
|
NestriFamilyMonthly: new sst.Secret("NestriFamilyMonthly"),
|
||||||
|
NestriFamilyYearly: new sst.Secret("NestriFamilyYearly"),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const allSecrets = Object.values(secret);
|
export const allSecrets = Object.values(secret);
|
||||||
2
packages/core/migrations/0006_worthless_dreadnoughts.sql
Normal file
2
packages/core/migrations/0006_worthless_dreadnoughts.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "member" ADD COLUMN "role" text NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "team" DROP COLUMN "plan_type";
|
||||||
15
packages/core/migrations/0007_warm_secret_warriors.sql
Normal file
15
packages/core/migrations/0007_warm_secret_warriors.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE "subscription" (
|
||||||
|
"id" char(30) NOT NULL,
|
||||||
|
"user_id" char(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"team_id" char(30) NOT NULL,
|
||||||
|
"standing" text NOT NULL,
|
||||||
|
"plan_type" text NOT NULL,
|
||||||
|
"tokens" integer NOT NULL,
|
||||||
|
"product_id" varchar(255),
|
||||||
|
"subscription_id" varchar(255)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_team_id_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
3
packages/core/migrations/0008_third_mindworm.sql
Normal file
3
packages/core/migrations/0008_third_mindworm.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_id_team_id_pk" PRIMARY KEY("id","team_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "subscription_id" ON "subscription" USING btree ("id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "subscription_user_id" ON "subscription" USING btree ("user_id");
|
||||||
2
packages/core/migrations/0009_luxuriant_wraith.sql
Normal file
2
packages/core/migrations/0009_luxuriant_wraith.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE UNIQUE INDEX "steam_id" ON "steam" USING btree ("steam_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "steam_user_id" ON "steam" USING btree ("user_id");
|
||||||
485
packages/core/migrations/meta/0006_snapshot.json
Normal file
485
packages/core/migrations/meta/0006_snapshot.json
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
{
|
||||||
|
"id": "69827225-1351-4709-a9b2-facb0f569215",
|
||||||
|
"prevId": "0b04858c-a7e3-43b6-98a4-1dc2f6f97488",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.machine": {
|
||||||
|
"name": "machine",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"name": "country",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"name": "timezone",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"name": "location",
|
||||||
|
"type": "point",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"fingerprint": {
|
||||||
|
"name": "fingerprint",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"country_code": {
|
||||||
|
"name": "country_code",
|
||||||
|
"type": "varchar(2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_fingerprint": {
|
||||||
|
"name": "machine_fingerprint",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "fingerprint",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.member": {
|
||||||
|
"name": "member",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"team_id": {
|
||||||
|
"name": "team_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"email_global": {
|
||||||
|
"name": "email_global",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"member_email": {
|
||||||
|
"name": "member_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "team_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"member_team_id_id_pk": {
|
||||||
|
"name": "member_team_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"team_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.steam": {
|
||||||
|
"name": "steam",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"last_seen": {
|
||||||
|
"name": "last_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"steam_id": {
|
||||||
|
"name": "steam_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"avatar_url": {
|
||||||
|
"name": "avatar_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"last_game": {
|
||||||
|
"name": "last_game",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"country_code": {
|
||||||
|
"name": "country_code",
|
||||||
|
"type": "varchar(2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"steam_email": {
|
||||||
|
"name": "steam_email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"persona_name": {
|
||||||
|
"name": "persona_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"limitation": {
|
||||||
|
"name": "limitation",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"steam_user_id_user_id_fk": {
|
||||||
|
"name": "steam_user_id_user_id_fk",
|
||||||
|
"tableFrom": "steam",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.team": {
|
||||||
|
"name": "team",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"avatar_url": {
|
||||||
|
"name": "avatar_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"discriminator": {
|
||||||
|
"name": "discriminator",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"polar_customer_id": {
|
||||||
|
"name": "polar_customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_polar_customer_id_unique": {
|
||||||
|
"name": "user_polar_customer_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"polar_customer_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
580
packages/core/migrations/meta/0007_snapshot.json
Normal file
580
packages/core/migrations/meta/0007_snapshot.json
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
{
|
||||||
|
"id": "fff2b73d-85ab-48bc-86de-69d3caf317f0",
|
||||||
|
"prevId": "69827225-1351-4709-a9b2-facb0f569215",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.machine": {
|
||||||
|
"name": "machine",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"name": "country",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"name": "timezone",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"name": "location",
|
||||||
|
"type": "point",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"fingerprint": {
|
||||||
|
"name": "fingerprint",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"country_code": {
|
||||||
|
"name": "country_code",
|
||||||
|
"type": "varchar(2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_fingerprint": {
|
||||||
|
"name": "machine_fingerprint",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "fingerprint",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.member": {
|
||||||
|
"name": "member",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"team_id": {
|
||||||
|
"name": "team_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"email_global": {
|
||||||
|
"name": "email_global",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"member_email": {
|
||||||
|
"name": "member_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "team_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"member_team_id_id_pk": {
|
||||||
|
"name": "member_team_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"team_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.steam": {
|
||||||
|
"name": "steam",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"last_seen": {
|
||||||
|
"name": "last_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"steam_id": {
|
||||||
|
"name": "steam_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"avatar_url": {
|
||||||
|
"name": "avatar_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"last_game": {
|
||||||
|
"name": "last_game",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"country_code": {
|
||||||
|
"name": "country_code",
|
||||||
|
"type": "varchar(2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"steam_email": {
|
||||||
|
"name": "steam_email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"persona_name": {
|
||||||
|
"name": "persona_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"limitation": {
|
||||||
|
"name": "limitation",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"steam_user_id_user_id_fk": {
|
||||||
|
"name": "steam_user_id_user_id_fk",
|
||||||
|
"tableFrom": "steam",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.subscription": {
|
||||||
|
"name": "subscription",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"team_id": {
|
||||||
|
"name": "team_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"standing": {
|
||||||
|
"name": "standing",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"plan_type": {
|
||||||
|
"name": "plan_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"tokens": {
|
||||||
|
"name": "tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"name": "product_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"subscription_id": {
|
||||||
|
"name": "subscription_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"subscription_team_id_team_id_fk": {
|
||||||
|
"name": "subscription_team_id_team_id_fk",
|
||||||
|
"tableFrom": "subscription",
|
||||||
|
"tableTo": "team",
|
||||||
|
"columnsFrom": [
|
||||||
|
"team_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.team": {
|
||||||
|
"name": "team",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"avatar_url": {
|
||||||
|
"name": "avatar_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"discriminator": {
|
||||||
|
"name": "discriminator",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"polar_customer_id": {
|
||||||
|
"name": "polar_customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_polar_customer_id_unique": {
|
||||||
|
"name": "user_polar_customer_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"polar_customer_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
619
packages/core/migrations/meta/0008_snapshot.json
Normal file
619
packages/core/migrations/meta/0008_snapshot.json
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
{
|
||||||
|
"id": "17b9c14f-ff15-44a5-9aaf-3f3b7dd7d294",
|
||||||
|
"prevId": "fff2b73d-85ab-48bc-86de-69d3caf317f0",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.machine": {
|
||||||
|
"name": "machine",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"name": "country",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"name": "timezone",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"name": "location",
|
||||||
|
"type": "point",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"fingerprint": {
|
||||||
|
"name": "fingerprint",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"country_code": {
|
||||||
|
"name": "country_code",
|
||||||
|
"type": "varchar(2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_fingerprint": {
|
||||||
|
"name": "machine_fingerprint",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "fingerprint",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.member": {
|
||||||
|
"name": "member",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"team_id": {
|
||||||
|
"name": "team_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"email_global": {
|
||||||
|
"name": "email_global",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"member_email": {
|
||||||
|
"name": "member_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "team_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"member_team_id_id_pk": {
|
||||||
|
"name": "member_team_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"team_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.steam": {
|
||||||
|
"name": "steam",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"last_seen": {
|
||||||
|
"name": "last_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"steam_id": {
|
||||||
|
"name": "steam_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"avatar_url": {
|
||||||
|
"name": "avatar_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"last_game": {
|
||||||
|
"name": "last_game",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"country_code": {
|
||||||
|
"name": "country_code",
|
||||||
|
"type": "varchar(2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"steam_email": {
|
||||||
|
"name": "steam_email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"persona_name": {
|
||||||
|
"name": "persona_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"limitation": {
|
||||||
|
"name": "limitation",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"steam_user_id_user_id_fk": {
|
||||||
|
"name": "steam_user_id_user_id_fk",
|
||||||
|
"tableFrom": "steam",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.subscription": {
|
||||||
|
"name": "subscription",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"team_id": {
|
||||||
|
"name": "team_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"standing": {
|
||||||
|
"name": "standing",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"plan_type": {
|
||||||
|
"name": "plan_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"tokens": {
|
||||||
|
"name": "tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"name": "product_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"subscription_id": {
|
||||||
|
"name": "subscription_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"subscription_id": {
|
||||||
|
"name": "subscription_id",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"subscription_user_id": {
|
||||||
|
"name": "subscription_user_id",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"subscription_team_id_team_id_fk": {
|
||||||
|
"name": "subscription_team_id_team_id_fk",
|
||||||
|
"tableFrom": "subscription",
|
||||||
|
"tableTo": "team",
|
||||||
|
"columnsFrom": [
|
||||||
|
"team_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"subscription_id_team_id_pk": {
|
||||||
|
"name": "subscription_id_team_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"id",
|
||||||
|
"team_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.team": {
|
||||||
|
"name": "team",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"avatar_url": {
|
||||||
|
"name": "avatar_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"discriminator": {
|
||||||
|
"name": "discriminator",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"polar_customer_id": {
|
||||||
|
"name": "polar_customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_polar_customer_id_unique": {
|
||||||
|
"name": "user_polar_customer_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"polar_customer_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
650
packages/core/migrations/meta/0009_snapshot.json
Normal file
650
packages/core/migrations/meta/0009_snapshot.json
Normal file
@@ -0,0 +1,650 @@
|
|||||||
|
{
|
||||||
|
"id": "1717c769-cee0-4242-bcbb-9538c80d985c",
|
||||||
|
"prevId": "17b9c14f-ff15-44a5-9aaf-3f3b7dd7d294",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.machine": {
|
||||||
|
"name": "machine",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"name": "country",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"name": "timezone",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"name": "location",
|
||||||
|
"type": "point",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"fingerprint": {
|
||||||
|
"name": "fingerprint",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"country_code": {
|
||||||
|
"name": "country_code",
|
||||||
|
"type": "varchar(2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"machine_fingerprint": {
|
||||||
|
"name": "machine_fingerprint",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "fingerprint",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.member": {
|
||||||
|
"name": "member",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"team_id": {
|
||||||
|
"name": "team_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"email_global": {
|
||||||
|
"name": "email_global",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"member_email": {
|
||||||
|
"name": "member_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "team_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"member_team_id_id_pk": {
|
||||||
|
"name": "member_team_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"team_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.steam": {
|
||||||
|
"name": "steam",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"last_seen": {
|
||||||
|
"name": "last_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"steam_id": {
|
||||||
|
"name": "steam_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"avatar_url": {
|
||||||
|
"name": "avatar_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"last_game": {
|
||||||
|
"name": "last_game",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"country_code": {
|
||||||
|
"name": "country_code",
|
||||||
|
"type": "varchar(2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"steam_email": {
|
||||||
|
"name": "steam_email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"persona_name": {
|
||||||
|
"name": "persona_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"limitation": {
|
||||||
|
"name": "limitation",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"steam_id": {
|
||||||
|
"name": "steam_id",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "steam_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"steam_user_id": {
|
||||||
|
"name": "steam_user_id",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"steam_user_id_user_id_fk": {
|
||||||
|
"name": "steam_user_id_user_id_fk",
|
||||||
|
"tableFrom": "steam",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.subscription": {
|
||||||
|
"name": "subscription",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"team_id": {
|
||||||
|
"name": "team_id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"standing": {
|
||||||
|
"name": "standing",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"plan_type": {
|
||||||
|
"name": "plan_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"tokens": {
|
||||||
|
"name": "tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"product_id": {
|
||||||
|
"name": "product_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"subscription_id": {
|
||||||
|
"name": "subscription_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"subscription_id": {
|
||||||
|
"name": "subscription_id",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"subscription_user_id": {
|
||||||
|
"name": "subscription_user_id",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"subscription_team_id_team_id_fk": {
|
||||||
|
"name": "subscription_team_id_team_id_fk",
|
||||||
|
"tableFrom": "subscription",
|
||||||
|
"tableTo": "team",
|
||||||
|
"columnsFrom": [
|
||||||
|
"team_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"subscription_id_team_id_pk": {
|
||||||
|
"name": "subscription_id_team_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"id",
|
||||||
|
"team_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.team": {
|
||||||
|
"name": "team",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "char(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_updated": {
|
||||||
|
"name": "time_updated",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"avatar_url": {
|
||||||
|
"name": "avatar_url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"discriminator": {
|
||||||
|
"name": "discriminator",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"polar_customer_id": {
|
||||||
|
"name": "polar_customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_polar_customer_id_unique": {
|
||||||
|
"name": "user_polar_customer_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"polar_customer_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,34 @@
|
|||||||
"when": 1744614896792,
|
"when": 1744614896792,
|
||||||
"tag": "0005_aspiring_stature",
|
"tag": "0005_aspiring_stature",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1744634229644,
|
||||||
|
"tag": "0006_worthless_dreadnoughts",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1744634322996,
|
||||||
|
"tag": "0007_warm_secret_warriors",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1744651530530,
|
||||||
|
"tag": "0008_third_mindworm",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1744651817581,
|
||||||
|
"tag": "0009_luxuriant_wraith",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -109,43 +109,34 @@ export function assertActor<T extends Actor["type"]>(type: T) {
|
|||||||
return actor as Extract<Actor, { type: T }>;
|
return actor as Extract<Actor, { type: T }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current actor's team ID.
|
||||||
|
*
|
||||||
|
* @returns The team ID associated with the current actor.
|
||||||
|
* @throws {VisibleError} If the current actor does not have a {@link teamID} property.
|
||||||
|
*/
|
||||||
export function useTeam() {
|
export function useTeam() {
|
||||||
const actor = useActor();
|
const actor = useActor();
|
||||||
if ("teamID" in actor.properties) return actor.properties.teamID;
|
if ("teamID" in actor.properties) return actor.properties.teamID;
|
||||||
throw new Error(`Expected actor to have teamID`);
|
throw new VisibleError(
|
||||||
}
|
"authentication",
|
||||||
|
ErrorCodes.Authentication.UNAUTHORIZED,
|
||||||
export function useMachine() {
|
`Expected actor to have teamID`
|
||||||
const actor = useActor();
|
);
|
||||||
if ("machineID" in actor.properties) return actor.properties.fingerprint;
|
|
||||||
throw new Error(`Expected actor to have fingerprint`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the current user possesses the specified flag.
|
* Returns the fingerprint of the current actor if the actor has a machine identity.
|
||||||
*
|
*
|
||||||
* This function executes a database transaction that queries the user table for the current user's flags.
|
* @returns The fingerprint of the current machine actor.
|
||||||
* If the flags are missing, it throws a {@link VisibleError} with the code {@link ErrorCodes.Validation.MISSING_REQUIRED_FIELD}
|
* @throws {VisibleError} If the current actor does not have a machine identity.
|
||||||
* and a message indicating that the required flag is absent.
|
|
||||||
*
|
|
||||||
* @param flag - The name of the user flag to verify.
|
|
||||||
*
|
|
||||||
* @throws {VisibleError} If the user's flag is missing.
|
|
||||||
*/
|
*/
|
||||||
export async function assertUserFlag(flag: keyof UserFlags) {
|
export function useMachine() {
|
||||||
return useTransaction((tx) =>
|
const actor = useActor();
|
||||||
tx
|
if ("machineID" in actor.properties) return actor.properties.fingerprint;
|
||||||
.select({ flags: userTable.flags })
|
|
||||||
.from(userTable)
|
|
||||||
.where(eq(userTable.id, useUserID()))
|
|
||||||
.then((rows) => {
|
|
||||||
const flags = rows[0]?.flags;
|
|
||||||
if (!flags)
|
|
||||||
throw new VisibleError(
|
throw new VisibleError(
|
||||||
"not_found",
|
"authentication",
|
||||||
ErrorCodes.Validation.MISSING_REQUIRED_FIELD,
|
ErrorCodes.Authentication.UNAUTHORIZED,
|
||||||
"Actor does not have " + flag + " flag",
|
`Expected actor to have fingerprint`
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import "zod-openapi/extend";
|
import "zod-openapi/extend";
|
||||||
|
|
||||||
export namespace Common {
|
export namespace Common {
|
||||||
export const IdDescription = `Unique object identifier.
|
export const IdDescription = `Unique object identifier.
|
||||||
The format and length of IDs may change over time.`;
|
The format and length of IDs may change over time.`;
|
||||||
|
|
||||||
|
export const now = () => sql`now()`;
|
||||||
|
export const utc = () => sql`now() at time zone 'utc'`;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { char, timestamp as rawTs } from "drizzle-orm/pg-core";
|
import { char, timestamp as rawTs } from "drizzle-orm/pg-core";
|
||||||
|
import { teamTable } from "../team/team.sql";
|
||||||
|
|
||||||
export const ulid = (name: string) => char(name, { length: 26 + 4 });
|
export const ulid = (name: string) => char(name, { length: 26 + 4 });
|
||||||
|
|
||||||
|
|||||||
@@ -34,23 +34,38 @@ export namespace Examples {
|
|||||||
steamAccounts: [Steam]
|
steamAccounts: [Steam]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Team = {
|
export const Product = {
|
||||||
id: Id("team"),
|
id: Id("product"),
|
||||||
name: "John Does' Team",
|
name: "RTX 4090",
|
||||||
slug: "john_doe",
|
description: "Ideal for dedicated gamers who crave more flexibility and social gaming experiences.",
|
||||||
planType: "BYOG" as const
|
tokensPerHour: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Subscription = {
|
||||||
|
tokens: 100,
|
||||||
|
id: Id("subscription"),
|
||||||
|
userID: Id("user"),
|
||||||
|
teamID: Id("team"),
|
||||||
|
planType: "pro" as const, // free, pro, family, enterprise
|
||||||
|
standing: "new" as const, // new, good, overdue, cancelled
|
||||||
|
polarProductID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
|
||||||
|
polarSubscriptionID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Member = {
|
export const Member = {
|
||||||
id: Id("member"),
|
id: Id("member"),
|
||||||
email: "john@example.com",
|
email: "john@example.com",
|
||||||
teamID: Id("team"),
|
teamID: Id("team"),
|
||||||
|
role: "admin" as const,
|
||||||
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
|
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Polar = {
|
export const Team = {
|
||||||
teamID: Id("team"),
|
id: Id("team"),
|
||||||
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
|
name: "John Does' Team",
|
||||||
|
slug: "john_doe",
|
||||||
|
subscriptions: [Subscription],
|
||||||
|
members: [Member]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Machine = {
|
export const Machine = {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Common } from "../common";
|
|||||||
import { createID, fn } from "../utils";
|
import { createID, fn } from "../utils";
|
||||||
import { createEvent } from "../event";
|
import { createEvent } from "../event";
|
||||||
import { Examples } from "../examples";
|
import { Examples } from "../examples";
|
||||||
import { memberTable } from "./member.sql";
|
import { memberTable, role } from "./member.sql";
|
||||||
import { and, eq, sql, asc, isNull } from "../drizzle";
|
import { and, eq, sql, asc, isNull } from "../drizzle";
|
||||||
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export namespace Member {
|
|||||||
description: Common.IdDescription,
|
description: Common.IdDescription,
|
||||||
example: Examples.Member.id,
|
example: Examples.Member.id,
|
||||||
}),
|
}),
|
||||||
timeSeen: z.date().or(z.null()).openapi({
|
timeSeen: z.date().nullable().or(z.undefined()).openapi({
|
||||||
description: "The last time this team member was active",
|
description: "The last time this team member was active",
|
||||||
example: Examples.Member.timeSeen
|
example: Examples.Member.timeSeen
|
||||||
}),
|
}),
|
||||||
@@ -25,6 +25,10 @@ export namespace Member {
|
|||||||
description: "The unique id of the team this member is on",
|
description: "The unique id of the team this member is on",
|
||||||
example: Examples.Member.teamID
|
example: Examples.Member.teamID
|
||||||
}),
|
}),
|
||||||
|
role: z.enum(role).openapi({
|
||||||
|
description: "The role of this team member",
|
||||||
|
example: Examples.Member.role
|
||||||
|
}),
|
||||||
email: z.string().openapi({
|
email: z.string().openapi({
|
||||||
description: "The email of this team member",
|
description: "The email of this team member",
|
||||||
example: Examples.Member.email
|
example: Examples.Member.email
|
||||||
@@ -68,6 +72,7 @@ export namespace Member {
|
|||||||
id,
|
id,
|
||||||
teamID: useTeam(),
|
teamID: useTeam(),
|
||||||
email: input.email,
|
email: input.email,
|
||||||
|
role: input.first ? "owner" : "member",
|
||||||
timeSeen: input.first ? sql`now()` : null,
|
timeSeen: input.first ? sql`now()` : null,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -113,11 +118,18 @@ export namespace Member {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a raw member database row into a standardized {@link Member.Info} object.
|
||||||
|
*
|
||||||
|
* @param input - The database row representing a member.
|
||||||
|
* @returns The member information formatted as a {@link Member.Info} object.
|
||||||
|
*/
|
||||||
export function serialize(
|
export function serialize(
|
||||||
input: typeof memberTable.$inferSelect,
|
input: typeof memberTable.$inferSelect,
|
||||||
): z.infer<typeof Info> {
|
): z.infer<typeof Info> {
|
||||||
return {
|
return {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
|
role: input.role,
|
||||||
email: input.email,
|
email: input.email,
|
||||||
teamID: input.teamID,
|
teamID: input.teamID,
|
||||||
timeSeen: input.timeSeen
|
timeSeen: input.timeSeen
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { teamIndexes } from "../team/team.sql";
|
import { teamIndexes } from "../team/team.sql";
|
||||||
import { timestamps, utc, teamID } from "../drizzle/types";
|
import { timestamps, utc, teamID } from "../drizzle/types";
|
||||||
import { index, pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core";
|
import { index, pgTable, text, uniqueIndex, varchar } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
export const role = ["admin", "member", "owner"] as const;
|
||||||
|
|
||||||
export const memberTable = pgTable(
|
export const memberTable = pgTable(
|
||||||
"member",
|
"member",
|
||||||
{
|
{
|
||||||
...teamID,
|
...teamID,
|
||||||
...timestamps,
|
...timestamps,
|
||||||
|
role: text("role", { enum: role }).notNull(),
|
||||||
timeSeen: utc("time_seen"),
|
timeSeen: utc("time_seen"),
|
||||||
email: varchar("email", { length: 255 }).notNull(),
|
email: varchar("email", { length: 255 }).notNull(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,69 +1,16 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fn } from "../utils";
|
import { fn } from "../utils";
|
||||||
import { Resource } from "sst";
|
import { Resource } from "sst";
|
||||||
import { eq, and } from "../drizzle";
|
import { useTeam, useUserID } from "../actor";
|
||||||
import { useTeam } from "../actor";
|
|
||||||
import { createEvent } from "../event";
|
|
||||||
// import { polarTable, Standing } from "./polar.sql.ts.test";
|
|
||||||
import { Polar as PolarSdk } from "@polar-sh/sdk";
|
import { Polar as PolarSdk } from "@polar-sh/sdk";
|
||||||
import { useTransaction } from "../drizzle/transaction";
|
import { validateEvent } from "@polar-sh/sdk/webhooks";
|
||||||
|
import { PlanType } from "../subscription/subscription.sql";
|
||||||
|
|
||||||
const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" });
|
const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" });
|
||||||
|
const planType = z.enum(PlanType)
|
||||||
export namespace Polar {
|
export namespace Polar {
|
||||||
export const client = polar;
|
export const client = polar;
|
||||||
|
|
||||||
export const Info = z.object({
|
|
||||||
teamID: z.string(),
|
|
||||||
subscriptionID: z.string().nullable(),
|
|
||||||
customerID: z.string(),
|
|
||||||
subscriptionItemID: z.string().nullable(),
|
|
||||||
// standing: z.enum(Standing),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Info = z.infer<typeof Info>;
|
|
||||||
|
|
||||||
export const Checkout = z.object({
|
|
||||||
annual: z.boolean().optional(),
|
|
||||||
successUrl: z.string(),
|
|
||||||
cancelUrl: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const CheckoutSession = z.object({
|
|
||||||
url: z.string().nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const CustomerSubscriptionEventType = [
|
|
||||||
"created",
|
|
||||||
"updated",
|
|
||||||
"deleted",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const Events = {
|
|
||||||
CustomerSubscriptionEvent: createEvent(
|
|
||||||
"polar.customer-subscription-event",
|
|
||||||
z.object({
|
|
||||||
type: z.enum(CustomerSubscriptionEventType),
|
|
||||||
status: z.string(),
|
|
||||||
teamID: z.string().min(1),
|
|
||||||
customerID: z.string().min(1),
|
|
||||||
subscriptionID: z.string().min(1),
|
|
||||||
subscriptionItemID: z.string().min(1),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// export function get() {
|
|
||||||
// return useTransaction(async (tx) =>
|
|
||||||
// tx
|
|
||||||
// .select()
|
|
||||||
// .from(polarTable)
|
|
||||||
// .where(eq(polarTable.teamID, useTeam()))
|
|
||||||
// .execute()
|
|
||||||
// .then((rows) => rows.map(serialize).at(0)),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const fromUserEmail = fn(z.string().min(1), async (email) => {
|
export const fromUserEmail = fn(z.string().min(1), async (email) => {
|
||||||
try {
|
try {
|
||||||
const customers = await client.customers.list({ email })
|
const customers = await client.customers.list({ email })
|
||||||
@@ -81,89 +28,69 @@ export namespace Polar {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// export const setCustomerID = fn(Info.shape.customerID, async (customerID) =>
|
const getProductIDs = (plan: z.infer<typeof planType>) => {
|
||||||
// useTransaction(async (tx) =>
|
switch (plan) {
|
||||||
// tx
|
case "free":
|
||||||
// .insert(polarTable)
|
return [Resource.NestriFreeMonthly.value]
|
||||||
// .values({
|
case "pro":
|
||||||
// teamID: useTeam(),
|
return [Resource.NestriProYearly.value, Resource.NestriProMonthly.value]
|
||||||
// customerID,
|
case "family":
|
||||||
// standing: "new",
|
return [Resource.NestriFamilyYearly.value, Resource.NestriFamilyMonthly.value]
|
||||||
// })
|
default:
|
||||||
// .execute(),
|
return [Resource.NestriFreeMonthly.value]
|
||||||
// ),
|
}
|
||||||
// );
|
}
|
||||||
|
|
||||||
// export const setSubscription = fn(
|
export const createPortal = fn(
|
||||||
// Info.pick({
|
z.string(),
|
||||||
// subscriptionID: true,
|
async (customerId) => {
|
||||||
// subscriptionItemID: true,
|
const session = await client.customerSessions.create({
|
||||||
// }),
|
customerId
|
||||||
// (input) =>
|
})
|
||||||
// useTransaction(async (tx) =>
|
|
||||||
// tx
|
return session.customerPortalUrl
|
||||||
// .update(polarTable)
|
}
|
||||||
// .set({
|
)
|
||||||
// subscriptionID: input.subscriptionID,
|
|
||||||
// subscriptionItemID: input.subscriptionItemID,
|
//TODO: Implement this
|
||||||
// })
|
export const handleWebhook = async(payload: ReturnType<typeof validateEvent>) => {
|
||||||
// .where(eq(polarTable.teamID, useTeam()))
|
switch (payload.type) {
|
||||||
// .returning()
|
case "subscription.created":
|
||||||
// .execute()
|
const teamID = payload.data.metadata.teamID
|
||||||
// .then((rows) => rows.map(serialize).at(0)),
|
}
|
||||||
// ),
|
}
|
||||||
// );
|
|
||||||
|
export const createCheckout = fn(
|
||||||
// export const removeSubscription = fn(
|
z
|
||||||
// z.string().min(1),
|
.object({
|
||||||
// (stripeSubscriptionID) =>
|
planType: z.enum(PlanType),
|
||||||
// useTransaction((tx) =>
|
customerEmail: z.string(),
|
||||||
// tx
|
successUrl: z.string(),
|
||||||
// .update(polarTable)
|
customerID: z.string(),
|
||||||
// .set({
|
allowDiscountCodes: z.boolean(),
|
||||||
// subscriptionItemID: null,
|
teamID: z.string()
|
||||||
// subscriptionID: null,
|
})
|
||||||
// })
|
.partial({
|
||||||
// .where(and(eq(polarTable.subscriptionID, stripeSubscriptionID)))
|
customerEmail: true,
|
||||||
// .execute(),
|
allowDiscountCodes: true,
|
||||||
// ),
|
customerID: true,
|
||||||
// );
|
teamID: true
|
||||||
|
}),
|
||||||
// export const setStanding = fn(
|
async (input) => {
|
||||||
// Info.pick({
|
const productIDs = getProductIDs(input.planType)
|
||||||
// subscriptionID: true,
|
|
||||||
// standing: true,
|
const checkoutUrl =
|
||||||
// }),
|
await client.checkouts.create({
|
||||||
// (input) =>
|
products: productIDs,
|
||||||
// useTransaction((tx) =>
|
customerEmail: input.customerEmail ?? useUserID(),
|
||||||
// tx
|
successUrl: `${input.successUrl}?checkout={CHECKOUT_ID}`,
|
||||||
// .update(polarTable)
|
allowDiscountCodes: input.allowDiscountCodes ?? false,
|
||||||
// .set({ standing: input.standing })
|
customerId: input.customerID,
|
||||||
// .where(and(eq(polarTable.subscriptionID, input.subscriptionID!)))
|
customerMetadata: {
|
||||||
// .execute(),
|
teamID: input.teamID ?? useTeam()
|
||||||
// ),
|
}
|
||||||
// );
|
})
|
||||||
|
|
||||||
// export const fromCustomerID = fn(Info.shape.customerID, (customerID) =>
|
return checkoutUrl.url
|
||||||
// useTransaction((tx) =>
|
})
|
||||||
// tx
|
|
||||||
// .select()
|
|
||||||
// .from(polarTable)
|
|
||||||
// .where(and(eq(polarTable.customerID, customerID)))
|
|
||||||
// .execute()
|
|
||||||
// .then((rows) => rows.map(serialize).at(0)),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// function serialize(
|
|
||||||
// input: typeof polarTable.$inferSelect,
|
|
||||||
// ): z.infer<typeof Info> {
|
|
||||||
// return {
|
|
||||||
// teamID: input.teamID,
|
|
||||||
// customerID: input.customerID,
|
|
||||||
// subscriptionID: input.subscriptionID,
|
|
||||||
// subscriptionItemID: input.subscriptionItemID,
|
|
||||||
// standing: input.standing,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { timestamps, teamID } from "../drizzle/types";
|
|
||||||
import { teamIndexes, teamTable } from "../team/team.sql";
|
|
||||||
import { pgTable, text, varchar } from "drizzle-orm/pg-core";
|
|
||||||
|
|
||||||
// FIXME: This is causing errors while trying to db push
|
|
||||||
export const Standing = ["new", "good", "overdue"] as const;
|
|
||||||
|
|
||||||
export const polarTable = pgTable(
|
|
||||||
"polar",
|
|
||||||
{
|
|
||||||
teamID: teamID.teamID.primaryKey().references(() => teamTable.id),
|
|
||||||
...timestamps,
|
|
||||||
customerID: varchar("customer_id", { length: 255 }).notNull(),
|
|
||||||
subscriptionID: varchar("subscription_id", { length: 255 }),
|
|
||||||
subscriptionItemID: varchar("subscription_item_id", {
|
|
||||||
length: 255,
|
|
||||||
}),
|
|
||||||
standing: text("standing", { enum: Standing }).notNull(),
|
|
||||||
},
|
|
||||||
(table) => [
|
|
||||||
...teamIndexes(table),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@@ -1,24 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { id, timestamps, ulid, userID, utc } from "../drizzle/types";
|
|
||||||
import { index, pgTable, integer, uniqueIndex, varchar, text, primaryKey, json } from "drizzle-orm/pg-core";
|
|
||||||
import { userTable } from "../user/user.sql";
|
import { userTable } from "../user/user.sql";
|
||||||
|
import { id, timestamps, ulid, utc } from "../drizzle/types";
|
||||||
|
import { index, pgTable, integer, uniqueIndex, varchar, text, json } from "drizzle-orm/pg-core";
|
||||||
// public string Username { get; set; } = string.Empty;
|
|
||||||
// public ulong SteamId { get; set; }
|
|
||||||
// public string Email { get; set; } = string.Empty;
|
|
||||||
// public string Country { get; set; } = string.Empty;
|
|
||||||
// public string PersonaName { get; set; } = string.Empty;
|
|
||||||
// public string AvatarUrl { get; set; } = string.Empty;
|
|
||||||
// public bool IsLimited { get; set; }
|
|
||||||
// public bool IsLocked { get; set; }
|
|
||||||
// public bool IsBanned { get; set; }
|
|
||||||
// public bool IsAllowedToInviteFriends { get; set; }
|
|
||||||
// public ulong GameId { get; set; }
|
|
||||||
// public string GamePlayingName { get; set; } = string.Empty;
|
|
||||||
// public DateTime LastLogOn { get; set; }
|
|
||||||
// public DateTime LastLogOff { get; set; }
|
|
||||||
// public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
|
|
||||||
export const LastGame = z.object({
|
export const LastGame = z.object({
|
||||||
gameID: z.number(),
|
gameID: z.number(),
|
||||||
@@ -54,5 +37,9 @@ export const steamTable = pgTable(
|
|||||||
steamEmail: varchar("steam_email", { length: 255 }).notNull(),
|
steamEmail: varchar("steam_email", { length: 255 }).notNull(),
|
||||||
personaName: varchar("persona_name", { length: 255 }).notNull(),
|
personaName: varchar("persona_name", { length: 255 }).notNull(),
|
||||||
limitation: json("limitation").$type<AccountLimitation>().notNull(),
|
limitation: json("limitation").$type<AccountLimitation>().notNull(),
|
||||||
}
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex("steam_id").on(table.steamID),
|
||||||
|
index("steam_user_id").on(table.userID),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
192
packages/core/src/subscription/index.ts
Normal file
192
packages/core/src/subscription/index.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { Common } from "../common";
|
||||||
|
import { Examples } from "../examples";
|
||||||
|
import { createID, fn } from "../utils";
|
||||||
|
import { eq, and, isNull } from "../drizzle";
|
||||||
|
import { useTeam, useUserID } from "../actor";
|
||||||
|
import { createTransaction, useTransaction } from "../drizzle/transaction";
|
||||||
|
import { PlanType, Standing, subscriptionTable } from "./subscription.sql";
|
||||||
|
|
||||||
|
export namespace Subscription {
|
||||||
|
export const Info = z.object({
|
||||||
|
id: z.string().openapi({
|
||||||
|
description: Common.IdDescription,
|
||||||
|
example: Examples.Subscription.id,
|
||||||
|
}),
|
||||||
|
polarSubscriptionID: z.string().nullable().or(z.undefined()).openapi({
|
||||||
|
description: "The unique id of the plan this subscription is on",
|
||||||
|
example: Examples.Subscription.polarSubscriptionID,
|
||||||
|
}),
|
||||||
|
teamID: z.string().openapi({
|
||||||
|
description: "The unique id of the team this subscription is for",
|
||||||
|
example: Examples.Subscription.teamID,
|
||||||
|
}),
|
||||||
|
userID: z.string().openapi({
|
||||||
|
description: "The unique id of the user who is paying this subscription",
|
||||||
|
example: Examples.Subscription.userID,
|
||||||
|
}),
|
||||||
|
polarProductID: z.string().nullable().or(z.undefined()).openapi({
|
||||||
|
description: "The unique id of the product this subscription is for",
|
||||||
|
example: Examples.Subscription.polarProductID,
|
||||||
|
}),
|
||||||
|
tokens: z.number().openapi({
|
||||||
|
description: "The number of tokens this subscription has left",
|
||||||
|
example: Examples.Subscription.tokens,
|
||||||
|
}),
|
||||||
|
planType: z.enum(PlanType).openapi({
|
||||||
|
description: "The type of plan this subscription is for",
|
||||||
|
example: Examples.Subscription.planType,
|
||||||
|
}),
|
||||||
|
standing: z.enum(Standing).openapi({
|
||||||
|
description: "The standing of this subscription",
|
||||||
|
example: Examples.Subscription.standing,
|
||||||
|
}),
|
||||||
|
}).openapi({
|
||||||
|
ref: "Subscription",
|
||||||
|
description: "Represents a subscription on Nestri",
|
||||||
|
example: Examples.Subscription
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Info = z.infer<typeof Info>;
|
||||||
|
|
||||||
|
export const create = fn(
|
||||||
|
Info
|
||||||
|
.partial({
|
||||||
|
teamID: true,
|
||||||
|
userID: true,
|
||||||
|
id: true,
|
||||||
|
standing: true,
|
||||||
|
planType: true,
|
||||||
|
polarProductID: true,
|
||||||
|
polarSubscriptionID: true,
|
||||||
|
}),
|
||||||
|
(input) =>
|
||||||
|
createTransaction(async (tx) => {
|
||||||
|
const id = input.id ?? createID("subscription");
|
||||||
|
|
||||||
|
await tx.insert(subscriptionTable).values({
|
||||||
|
id,
|
||||||
|
tokens: input.tokens,
|
||||||
|
polarProductID: input.polarProductID ?? null,
|
||||||
|
polarSubscriptionID: input.polarSubscriptionID ?? null,
|
||||||
|
standing: input.standing ?? "new",
|
||||||
|
planType: input.planType ?? "free",
|
||||||
|
userID: input.userID ?? useUserID(),
|
||||||
|
teamID: input.teamID ?? useTeam(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return id;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const setPolarProductID = fn(
|
||||||
|
Info.pick({
|
||||||
|
id: true,
|
||||||
|
polarProductID: true,
|
||||||
|
}),
|
||||||
|
(input) =>
|
||||||
|
useTransaction(async (tx) =>
|
||||||
|
tx.update(subscriptionTable)
|
||||||
|
.set({
|
||||||
|
polarProductID: input.polarProductID,
|
||||||
|
})
|
||||||
|
.where(eq(subscriptionTable.id, input.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const setPolarSubscriptionID = fn(
|
||||||
|
Info.pick({
|
||||||
|
id: true,
|
||||||
|
polarSubscriptionID: true,
|
||||||
|
}),
|
||||||
|
(input) =>
|
||||||
|
useTransaction(async (tx) =>
|
||||||
|
tx.update(subscriptionTable)
|
||||||
|
.set({
|
||||||
|
polarSubscriptionID: input.polarSubscriptionID,
|
||||||
|
})
|
||||||
|
.where(eq(subscriptionTable.id, input.id))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const fromID = fn(z.string(), async (id) =>
|
||||||
|
useTransaction(async (tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(subscriptionTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(subscriptionTable.id, id),
|
||||||
|
isNull(subscriptionTable.timeDeleted)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(subscriptionTable.timeCreated)
|
||||||
|
.then((rows) => rows.map(serialize))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
export const fromTeamID = fn(z.string(), async (teamID) =>
|
||||||
|
useTransaction(async (tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(subscriptionTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(subscriptionTable.teamID, teamID),
|
||||||
|
isNull(subscriptionTable.timeDeleted)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(subscriptionTable.timeCreated)
|
||||||
|
.then((rows) => rows.map(serialize))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const fromUserID = fn(z.string(), async (userID) =>
|
||||||
|
useTransaction(async (tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(subscriptionTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(subscriptionTable.userID, userID),
|
||||||
|
isNull(subscriptionTable.timeDeleted)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(subscriptionTable.timeCreated)
|
||||||
|
.then((rows) => rows.map(serialize))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
export const remove = fn(Info.shape.id, (id) =>
|
||||||
|
useTransaction(async (tx) =>
|
||||||
|
tx
|
||||||
|
.update(subscriptionTable)
|
||||||
|
.set({
|
||||||
|
timeDeleted: Common.now(),
|
||||||
|
})
|
||||||
|
.where(eq(subscriptionTable.id, id))
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a raw subscription database record into a structured {@link Info} object.
|
||||||
|
*
|
||||||
|
* @param input - The subscription record retrieved from the database.
|
||||||
|
* @returns The subscription data formatted according to the {@link Info} schema.
|
||||||
|
*/
|
||||||
|
export function serialize(
|
||||||
|
input: typeof subscriptionTable.$inferSelect
|
||||||
|
): z.infer<typeof Info> {
|
||||||
|
return {
|
||||||
|
id: input.id,
|
||||||
|
userID: input.userID,
|
||||||
|
teamID: input.teamID,
|
||||||
|
standing: input.standing,
|
||||||
|
planType: input.planType,
|
||||||
|
tokens: input.tokens,
|
||||||
|
polarProductID: input.polarProductID,
|
||||||
|
polarSubscriptionID: input.polarSubscriptionID,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
31
packages/core/src/subscription/subscription.sql.ts
Normal file
31
packages/core/src/subscription/subscription.sql.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { teamTable } from "../team/team.sql";
|
||||||
|
import { ulid, userID, timestamps } from "../drizzle/types";
|
||||||
|
import { index, integer, pgTable, primaryKey, text, uniqueIndex, varchar } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
export const Standing = ["new", "good", "overdue", "cancelled"] as const;
|
||||||
|
export const PlanType = ["free", "pro", "family", "enterprise"] as const;
|
||||||
|
|
||||||
|
export const subscriptionTable = pgTable(
|
||||||
|
"subscription",
|
||||||
|
{
|
||||||
|
...userID,
|
||||||
|
...timestamps,
|
||||||
|
teamID: ulid("team_id")
|
||||||
|
.references(() => teamTable.id, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
standing: text("standing", { enum: Standing })
|
||||||
|
.notNull(),
|
||||||
|
planType: text("plan_type", { enum: PlanType })
|
||||||
|
.notNull(),
|
||||||
|
tokens: integer("tokens").notNull(),
|
||||||
|
polarProductID: varchar("product_id", { length: 255 }),
|
||||||
|
polarSubscriptionID: varchar("subscription_id", { length: 255 }),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
uniqueIndex("subscription_id").on(table.id),
|
||||||
|
index("subscription_user_id").on(table.userID),
|
||||||
|
primaryKey({
|
||||||
|
columns: [table.id, table.teamID]
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Resource } from "sst";
|
|
||||||
import { bus } from "sst/aws/bus";
|
|
||||||
import { Common } from "../common";
|
import { Common } from "../common";
|
||||||
|
import { Member } from "../member";
|
||||||
|
import { teamTable } from "./team.sql";
|
||||||
import { Examples } from "../examples";
|
import { Examples } from "../examples";
|
||||||
|
import { assertActor } from "../actor";
|
||||||
import { createEvent } from "../event";
|
import { createEvent } from "../event";
|
||||||
import { createID, fn } from "../utils";
|
import { createID, fn } from "../utils";
|
||||||
|
import { Subscription } from "../subscription";
|
||||||
import { and, eq, sql, isNull } from "../drizzle";
|
import { and, eq, sql, isNull } from "../drizzle";
|
||||||
import { PlanType, teamTable } from "./team.sql";
|
|
||||||
import { assertActor, withActor } from "../actor";
|
|
||||||
import { memberTable } from "../member/member.sql";
|
import { memberTable } from "../member/member.sql";
|
||||||
import { ErrorCodes, VisibleError } from "../error";
|
import { ErrorCodes, VisibleError } from "../error";
|
||||||
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
import { groupBy, map, pipe, values } from "remeda";
|
||||||
|
import { subscriptionTable } from "../subscription/subscription.sql";
|
||||||
|
import { createTransaction, useTransaction } from "../drizzle/transaction";
|
||||||
|
|
||||||
export namespace Team {
|
export namespace Team {
|
||||||
export const Info = z
|
export const Info = z
|
||||||
@@ -19,6 +21,7 @@ export namespace Team {
|
|||||||
description: Common.IdDescription,
|
description: Common.IdDescription,
|
||||||
example: Examples.Team.id,
|
example: Examples.Team.id,
|
||||||
}),
|
}),
|
||||||
|
// Remove spaces and make sure it is lowercase (this is just to make sure the frontend did this)
|
||||||
slug: z.string().regex(/^[a-z0-9\-]+$/, "Use a URL friendly name.").openapi({
|
slug: z.string().regex(/^[a-z0-9\-]+$/, "Use a URL friendly name.").openapi({
|
||||||
description: "The unique and url-friendly slug of this team",
|
description: "The unique and url-friendly slug of this team",
|
||||||
example: Examples.Team.slug
|
example: Examples.Team.slug
|
||||||
@@ -27,10 +30,14 @@ export namespace Team {
|
|||||||
description: "The name of this team",
|
description: "The name of this team",
|
||||||
example: Examples.Team.name
|
example: Examples.Team.name
|
||||||
}),
|
}),
|
||||||
planType: z.enum(PlanType).openapi({
|
members: Member.Info.array().openapi({
|
||||||
description: "The type of Plan this team is subscribed to",
|
description: "The members of this team",
|
||||||
example: Examples.Team.planType
|
example: Examples.Team.members
|
||||||
})
|
}),
|
||||||
|
subscriptions: Subscription.Info.array().openapi({
|
||||||
|
description: "The subscriptions of this team",
|
||||||
|
example: Examples.Team.subscriptions
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
ref: "Team",
|
ref: "Team",
|
||||||
@@ -60,16 +67,14 @@ export namespace Team {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const create = fn(
|
export const create = fn(
|
||||||
Info.pick({ slug: true, id: true, name: true, planType: true }).partial({
|
Info.pick({ slug: true, id: true, name: true, }).partial({
|
||||||
id: true,
|
id: true,
|
||||||
}), (input) =>
|
}), (input) =>
|
||||||
createTransaction(async (tx) => {
|
createTransaction(async (tx) => {
|
||||||
const id = input.id ?? createID("team");
|
const id = input.id ?? createID("team");
|
||||||
const result = await tx.insert(teamTable).values({
|
const result = await tx.insert(teamTable).values({
|
||||||
id,
|
id,
|
||||||
//Remove spaces and make sure it is lowercase (this is just to make sure the frontend did this)
|
slug: input.slug,
|
||||||
slug: input.slug, //.toLowerCase().replace(/[\s]/g, ''),
|
|
||||||
planType: input.planType,
|
|
||||||
name: input.name
|
name: input.name
|
||||||
})
|
})
|
||||||
.onConflictDoNothing({ target: teamTable.slug })
|
.onConflictDoNothing({ target: teamTable.slug })
|
||||||
@@ -80,6 +85,7 @@ export namespace Team {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//TODO: "Delete" subscription and member(s) as well
|
||||||
export const remove = fn(Info.shape.id, (input) =>
|
export const remove = fn(Info.shape.id, (input) =>
|
||||||
useTransaction(async (tx) => {
|
useTransaction(async (tx) => {
|
||||||
const account = assertActor("user");
|
const account = assertActor("user");
|
||||||
@@ -106,48 +112,107 @@ export namespace Team {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const list = fn(z.void(), () =>
|
export const list = fn(z.void(), () => {
|
||||||
useTransaction((tx) =>
|
const actor = assertActor("user");
|
||||||
|
return useTransaction(async (tx) =>
|
||||||
tx
|
tx
|
||||||
.select()
|
.select()
|
||||||
.from(teamTable)
|
.from(teamTable)
|
||||||
.where(isNull(teamTable.timeDeleted))
|
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
|
||||||
|
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(memberTable.email, actor.properties.email),
|
||||||
|
isNull(memberTable.timeDeleted),
|
||||||
|
isNull(teamTable.timeDeleted),
|
||||||
|
),
|
||||||
|
)
|
||||||
.execute()
|
.execute()
|
||||||
.then((rows) => rows.map(serialize)),
|
.then((rows) => serialize(rows))
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fromID = fn(z.string().min(1), async (id) =>
|
||||||
|
useTransaction(async (tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(teamTable)
|
||||||
|
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
|
||||||
|
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(teamTable.id, id),
|
||||||
|
isNull(memberTable.timeDeleted),
|
||||||
|
isNull(teamTable.timeDeleted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
.then((rows) => serialize(rows).at(0))
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const fromID = fn(z.string().min(1), async (id) =>
|
export const fromSlug = fn(z.string().min(1), async (slug) =>
|
||||||
useTransaction(async (tx) => {
|
useTransaction(async (tx) =>
|
||||||
return tx
|
tx
|
||||||
.select()
|
.select()
|
||||||
.from(teamTable)
|
.from(teamTable)
|
||||||
.where(and(eq(teamTable.id, id), isNull(teamTable.timeDeleted)))
|
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
|
||||||
|
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(teamTable.slug, slug),
|
||||||
|
isNull(memberTable.timeDeleted),
|
||||||
|
isNull(teamTable.timeDeleted),
|
||||||
|
),
|
||||||
|
)
|
||||||
.execute()
|
.execute()
|
||||||
.then((rows) => rows.map(serialize).at(0))
|
.then((rows) => serialize(rows).at(0))
|
||||||
}),
|
),
|
||||||
);
|
|
||||||
|
|
||||||
export const fromSlug = fn(z.string().min(1), async (input) =>
|
|
||||||
useTransaction(async (tx) => {
|
|
||||||
return tx
|
|
||||||
.select()
|
|
||||||
.from(teamTable)
|
|
||||||
.where(and(eq(teamTable.slug, input), isNull(teamTable.timeDeleted)))
|
|
||||||
.execute()
|
|
||||||
.then((rows) => rows.map(serialize).at(0))
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms an array of team, subscription, and member records into structured team objects.
|
||||||
|
*
|
||||||
|
* Groups input rows by team ID and constructs an array of team objects, each including its associated members and subscriptions.
|
||||||
|
*
|
||||||
|
* @param input - Array of objects containing team, subscription, and member data.
|
||||||
|
* @returns An array of team objects with their members and subscriptions.
|
||||||
|
*/
|
||||||
export function serialize(
|
export function serialize(
|
||||||
input: typeof teamTable.$inferSelect,
|
input: { team: typeof teamTable.$inferSelect, subscription: typeof subscriptionTable.$inferInsert | null, member: typeof memberTable.$inferInsert | null }[],
|
||||||
): z.infer<typeof Info> {
|
): z.infer<typeof Info>[] {
|
||||||
return {
|
console.log("serialize", input)
|
||||||
id: input.id,
|
return pipe(
|
||||||
name: input.name,
|
input,
|
||||||
slug: input.slug,
|
groupBy((row) => row.team.id),
|
||||||
planType: input.planType,
|
values(),
|
||||||
};
|
map((group) => ({
|
||||||
|
name: group[0].team.name,
|
||||||
|
id: group[0].team.id,
|
||||||
|
slug: group[0].team.slug,
|
||||||
|
subscriptions: !group[0].subscription ?
|
||||||
|
[] :
|
||||||
|
group.map((row) => ({
|
||||||
|
planType: row.subscription!.planType,
|
||||||
|
polarProductID: row.subscription!.polarProductID,
|
||||||
|
polarSubscriptionID: row.subscription!.polarSubscriptionID,
|
||||||
|
standing: row.subscription!.standing,
|
||||||
|
tokens: row.subscription!.tokens,
|
||||||
|
teamID: row.subscription!.teamID,
|
||||||
|
userID: row.subscription!.userID,
|
||||||
|
id: row.subscription!.id,
|
||||||
|
})),
|
||||||
|
members:
|
||||||
|
!group[0].member ?
|
||||||
|
[] :
|
||||||
|
group.map((row) => ({
|
||||||
|
id: row.member!.id,
|
||||||
|
email: row.member!.email,
|
||||||
|
role: row.member!.role,
|
||||||
|
teamID: row.member!.teamID,
|
||||||
|
timeSeen: row.member!.timeSeen,
|
||||||
|
}))
|
||||||
|
})),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
import { } from "drizzle-orm/postgres-js";
|
|
||||||
import { timestamps, id } from "../drizzle/types";
|
import { timestamps, id } from "../drizzle/types";
|
||||||
import {
|
import {
|
||||||
varchar,
|
varchar,
|
||||||
pgTable,
|
pgTable,
|
||||||
primaryKey,
|
primaryKey,
|
||||||
uniqueIndex,
|
uniqueIndex,
|
||||||
text
|
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const PlanType = ["Hosted", "BYOG"] as const;
|
|
||||||
|
|
||||||
export const teamTable = pgTable(
|
export const teamTable = pgTable(
|
||||||
"team",
|
"team",
|
||||||
{
|
{
|
||||||
@@ -17,7 +13,6 @@ export const teamTable = pgTable(
|
|||||||
...timestamps,
|
...timestamps,
|
||||||
name: varchar("name", { length: 255 }).notNull(),
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
slug: varchar("slug", { length: 255 }).notNull(),
|
slug: varchar("slug", { length: 255 }).notNull(),
|
||||||
planType: text("plan_type", { enum: PlanType }).notNull()
|
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
uniqueIndex("slug").on(table.slug)
|
uniqueIndex("slug").on(table.slug)
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Team } from "../team";
|
import { Team } from "../team";
|
||||||
import { bus } from "sst/aws/bus";
|
import { bus } from "sst/aws/bus";
|
||||||
|
import { Steam } from "../steam";
|
||||||
import { Common } from "../common";
|
import { Common } from "../common";
|
||||||
import { Polar } from "../polar/index";
|
import { Polar } from "../polar/index";
|
||||||
import { createID, fn } from "../utils";
|
import { createID, fn } from "../utils";
|
||||||
import { userTable } from "./user.sql";
|
import { userTable } from "./user.sql";
|
||||||
import { createEvent } from "../event";
|
import { createEvent } from "../event";
|
||||||
import { pipe, groupBy, values, map } from "remeda";
|
|
||||||
import { Examples } from "../examples";
|
import { Examples } from "../examples";
|
||||||
import { Resource } from "sst/resource";
|
import { Resource } from "sst/resource";
|
||||||
import { teamTable } from "../team/team.sql";
|
import { teamTable } from "../team/team.sql";
|
||||||
import { steamTable } from "../steam/steam.sql";
|
import { steamTable } from "../steam/steam.sql";
|
||||||
import { assertActor, withActor } from "../actor";
|
import { assertActor, withActor } from "../actor";
|
||||||
import { memberTable } from "../member/member.sql";
|
import { memberTable } from "../member/member.sql";
|
||||||
import { and, eq, isNull, asc, getTableColumns, sql } from "../drizzle";
|
import { pipe, groupBy, values, map } from "remeda";
|
||||||
|
import { and, eq, isNull, asc, sql } from "../drizzle";
|
||||||
|
import { subscriptionTable } from "../subscription/subscription.sql";
|
||||||
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
||||||
import { Steam } from "../steam";
|
|
||||||
|
|
||||||
|
|
||||||
export namespace User {
|
export namespace User {
|
||||||
@@ -154,91 +155,27 @@ export namespace User {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const fromEmail = fn(z.string(), async (email) =>
|
export const fromEmail = fn(z.string(), async (email) =>
|
||||||
useTransaction(async (tx) => {
|
useTransaction(async (tx) =>
|
||||||
const rows = await tx
|
tx
|
||||||
.select()
|
.select()
|
||||||
.from(userTable)
|
.from(userTable)
|
||||||
.leftJoin(steamTable, eq(userTable.id, steamTable.userID))
|
.leftJoin(steamTable, eq(userTable.id, steamTable.userID))
|
||||||
.where(and(eq(userTable.email, email), isNull(userTable.timeDeleted)))
|
.where(and(eq(userTable.email, email), isNull(userTable.timeDeleted)))
|
||||||
.orderBy(asc(userTable.timeCreated))
|
.orderBy(asc(userTable.timeCreated))
|
||||||
|
.then((rows => serialize(rows).at(0)))
|
||||||
const result = pipe(
|
|
||||||
rows,
|
|
||||||
groupBy((row) => row.user.id),
|
|
||||||
values(),
|
|
||||||
map(
|
|
||||||
(group): Info => ({
|
|
||||||
id: group[0].user.id,
|
|
||||||
name: group[0].user.name,
|
|
||||||
email: group[0].user.email,
|
|
||||||
avatarUrl: group[0].user.avatarUrl,
|
|
||||||
discriminator: group[0].user.discriminator,
|
|
||||||
polarCustomerID: group[0].user.polarCustomerID,
|
|
||||||
steamAccounts: !group[0].steam ?
|
|
||||||
[] :
|
|
||||||
group.map((row) => ({
|
|
||||||
id: row.steam!.id,
|
|
||||||
userID: row.steam!.userID,
|
|
||||||
steamID: row.steam!.steamID,
|
|
||||||
lastSeen: row.steam!.lastSeen,
|
|
||||||
avatarUrl: row.steam!.avatarUrl,
|
|
||||||
lastGame: row.steam!.lastGame,
|
|
||||||
username: row.steam!.username,
|
|
||||||
countryCode: row.steam!.countryCode,
|
|
||||||
steamEmail: row.steam!.steamEmail,
|
|
||||||
personaName: row.steam!.personaName,
|
|
||||||
limitation: row.steam!.limitation,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return result[0]
|
export const fromID = fn(z.string(), (id) =>
|
||||||
}),
|
useTransaction(async (tx) =>
|
||||||
)
|
tx
|
||||||
|
|
||||||
export const fromID = fn(z.string(), async (id) =>
|
|
||||||
useTransaction(async (tx) => {
|
|
||||||
const rows = await tx
|
|
||||||
.select()
|
.select()
|
||||||
.from(userTable)
|
.from(userTable)
|
||||||
.leftJoin(steamTable, eq(userTable.id, steamTable.userID))
|
.leftJoin(steamTable, eq(userTable.id, steamTable.userID))
|
||||||
.where(and(eq(userTable.id, id), isNull(userTable.timeDeleted)))
|
.where(and(eq(userTable.id, id), isNull(userTable.timeDeleted), isNull(steamTable.timeDeleted)))
|
||||||
.orderBy(asc(userTable.timeCreated))
|
.orderBy(asc(userTable.timeCreated))
|
||||||
|
.then((rows) => serialize(rows).at(0))
|
||||||
const result = pipe(
|
),
|
||||||
rows,
|
|
||||||
groupBy((row) => row.user.id),
|
|
||||||
values(),
|
|
||||||
map(
|
|
||||||
(group): Info => ({
|
|
||||||
id: group[0].user.id,
|
|
||||||
name: group[0].user.name,
|
|
||||||
email: group[0].user.email,
|
|
||||||
avatarUrl: group[0].user.avatarUrl,
|
|
||||||
discriminator: group[0].user.discriminator,
|
|
||||||
polarCustomerID: group[0].user.polarCustomerID,
|
|
||||||
steamAccounts: !group[0].steam ?
|
|
||||||
[] :
|
|
||||||
group.map((row) => ({
|
|
||||||
id: row.steam!.id,
|
|
||||||
userID: row.steam!.userID,
|
|
||||||
steamID: row.steam!.steamID,
|
|
||||||
lastSeen: row.steam!.lastSeen,
|
|
||||||
avatarUrl: row.steam!.avatarUrl,
|
|
||||||
lastGame: row.steam!.lastGame,
|
|
||||||
username: row.steam!.username,
|
|
||||||
countryCode: row.steam!.countryCode,
|
|
||||||
steamEmail: row.steam!.steamEmail,
|
|
||||||
personaName: row.steam!.personaName,
|
|
||||||
limitation: row.steam!.limitation,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return result[0]
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export const remove = fn(Info.shape.id, (id) =>
|
export const remove = fn(Info.shape.id, (id) =>
|
||||||
@@ -254,12 +191,54 @@ export namespace User {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an array of user and Steam account records into structured user objects with associated Steam accounts.
|
||||||
|
*
|
||||||
|
* @param input - An array of objects containing user data and optional Steam account data.
|
||||||
|
* @returns An array of user objects, each including a list of their associated Steam accounts.
|
||||||
|
*/
|
||||||
|
export function serialize(
|
||||||
|
input: { user: typeof userTable.$inferSelect; steam: typeof steamTable.$inferSelect | null }[],
|
||||||
|
): z.infer<typeof Info>[] {
|
||||||
|
return pipe(
|
||||||
|
input,
|
||||||
|
groupBy((row) => row.user.id),
|
||||||
|
values(),
|
||||||
|
map((group) => ({
|
||||||
|
...group[0].user,
|
||||||
|
steamAccounts: !group[0].steam ?
|
||||||
|
[] :
|
||||||
|
group.map((row) => ({
|
||||||
|
id: row.steam!.id,
|
||||||
|
lastSeen: row.steam!.lastSeen,
|
||||||
|
countryCode: row.steam!.countryCode,
|
||||||
|
username: row.steam!.username,
|
||||||
|
steamID: row.steam!.steamID,
|
||||||
|
lastGame: row.steam!.lastGame,
|
||||||
|
limitation: row.steam!.limitation,
|
||||||
|
steamEmail: row.steam!.steamEmail,
|
||||||
|
userID: row.steam!.userID,
|
||||||
|
personaName: row.steam!.personaName,
|
||||||
|
avatarUrl: row.steam!.avatarUrl,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the list of teams that the current user belongs to.
|
||||||
|
*
|
||||||
|
* @returns An array of team information objects representing the user's active team memberships.
|
||||||
|
*
|
||||||
|
* @remark Only teams and memberships that have not been deleted are included in the result.
|
||||||
|
*/
|
||||||
export function teams() {
|
export function teams() {
|
||||||
const actor = assertActor("user");
|
const actor = assertActor("user");
|
||||||
return useTransaction((tx) =>
|
return useTransaction(async (tx) =>
|
||||||
tx
|
tx
|
||||||
.select(getTableColumns(teamTable))
|
.select()
|
||||||
.from(teamTable)
|
.from(teamTable)
|
||||||
|
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
|
||||||
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
|
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@@ -269,7 +248,7 @@ export namespace User {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.execute()
|
.execute()
|
||||||
.then((rows) => rows.map(Team.serialize))
|
.then((rows) => Team.serialize(rows))
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,14 @@ import { ulid } from "ulid";
|
|||||||
|
|
||||||
export const prefixes = {
|
export const prefixes = {
|
||||||
user: "usr",
|
user: "usr",
|
||||||
team: "tea",
|
team: "tem",
|
||||||
task: "tsk",
|
task: "tsk",
|
||||||
machine: "mch",
|
machine: "mch",
|
||||||
member: "mbr",
|
member: "mbr",
|
||||||
steam: "stm",
|
steam: "stm",
|
||||||
|
subscription: "sub",
|
||||||
|
invite: "inv",
|
||||||
|
product: "prd",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -43,11 +43,6 @@ export const auth: MiddlewareHandler = async (c, next) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.subject.type === "machine") {
|
|
||||||
console.log("machine detected")
|
|
||||||
return withActor(result.subject, next);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.subject.type === "user") {
|
if (result.subject.type === "user") {
|
||||||
const teamID = c.req.header("x-nestri-team");
|
const teamID = c.req.header("x-nestri-team");
|
||||||
if (!teamID) return withActor(result.subject, next);
|
if (!teamID) return withActor(result.subject, next);
|
||||||
@@ -58,12 +53,11 @@ export const auth: MiddlewareHandler = async (c, next) => {
|
|||||||
teamID,
|
teamID,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async () => {
|
async () =>
|
||||||
return withActor(
|
withActor(
|
||||||
result.subject,
|
result.subject,
|
||||||
next,
|
next,
|
||||||
);
|
)
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -3,7 +3,7 @@ import { Hono } from "hono";
|
|||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { cors } from "hono/cors";
|
import { cors } from "hono/cors";
|
||||||
import { TeamApi } from "./team";
|
import { TeamApi } from "./team";
|
||||||
import { SteamApi } from "./steam";
|
import { PolarApi } from "./polar";
|
||||||
import { logger } from "hono/logger";
|
import { logger } from "hono/logger";
|
||||||
import { Realtime } from "./realtime";
|
import { Realtime } from "./realtime";
|
||||||
import { AccountApi } from "./account";
|
import { AccountApi } from "./account";
|
||||||
@@ -27,7 +27,7 @@ const routes = app
|
|||||||
.get("/", (c) => c.text("Hello World!"))
|
.get("/", (c) => c.text("Hello World!"))
|
||||||
.route("/realtime", Realtime.route)
|
.route("/realtime", Realtime.route)
|
||||||
.route("/team", TeamApi.route)
|
.route("/team", TeamApi.route)
|
||||||
.route("/steam", SteamApi.route)
|
.route("/polar", PolarApi.route)
|
||||||
.route("/account", AccountApi.route)
|
.route("/account", AccountApi.route)
|
||||||
.route("/machine", MachineApi.route)
|
.route("/machine", MachineApi.route)
|
||||||
.onError((error, c) => {
|
.onError((error, c) => {
|
||||||
|
|||||||
174
packages/functions/src/api/polar.ts
Normal file
174
packages/functions/src/api/polar.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import { Resource } from "sst";
|
||||||
|
import { notPublic } from "./auth";
|
||||||
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { User } from "@nestri/core/user/index";
|
||||||
|
import { assertActor } from "@nestri/core/actor";
|
||||||
|
import { Polar } from "@nestri/core/polar/index";
|
||||||
|
import { Examples } from "@nestri/core/examples";
|
||||||
|
import { ErrorResponses, Result, validator } from "./common";
|
||||||
|
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||||
|
import { PlanType } from "@nestri/core/subscription/subscription.sql";
|
||||||
|
import { WebhookVerificationError, validateEvent } from "@polar-sh/sdk/webhooks";
|
||||||
|
|
||||||
|
export namespace PolarApi {
|
||||||
|
export const route = new Hono()
|
||||||
|
.use(notPublic)
|
||||||
|
.get("/",
|
||||||
|
describeRoute({
|
||||||
|
tags: ["Polar"],
|
||||||
|
summary: "Create a Polar.sh customer portal",
|
||||||
|
description: "Creates Polar.sh's customer portal url where the user can manage their payments",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: Result(
|
||||||
|
z.object({
|
||||||
|
portalUrl: z.string()
|
||||||
|
}).openapi({
|
||||||
|
description: "The customer portal url",
|
||||||
|
example: { portalUrl: "https://polar.sh/portal/39393jdie09292" }
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: "customer portal url"
|
||||||
|
},
|
||||||
|
400: ErrorResponses[400],
|
||||||
|
404: ErrorResponses[404],
|
||||||
|
429: ErrorResponses[429],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
const actor = assertActor("user");
|
||||||
|
|
||||||
|
const user = await User.fromID(actor.properties.userID);
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
throw new VisibleError(
|
||||||
|
"not_found",
|
||||||
|
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
|
||||||
|
"User not found",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user.polarCustomerID)
|
||||||
|
throw new VisibleError(
|
||||||
|
"not_found",
|
||||||
|
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
|
||||||
|
"User does not contain Polar customer ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
const portalUrl = await Polar.createPortal(user.polarCustomerID)
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
data: {
|
||||||
|
portalUrl
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.post("/checkout",
|
||||||
|
describeRoute({
|
||||||
|
tags: ["Polar"],
|
||||||
|
summary: "Create a checkout url",
|
||||||
|
description: "Creates a Polar.sh's checkout url for the user to pay a subscription for this team",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: Result(
|
||||||
|
z.object({
|
||||||
|
checkoutUrl: z.string()
|
||||||
|
}).openapi({
|
||||||
|
description: "The checkout url",
|
||||||
|
example: { checkoutUrl: "https://polar.sh/portal/39393jdie09292" }
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: "checkout url"
|
||||||
|
},
|
||||||
|
400: ErrorResponses[400],
|
||||||
|
404: ErrorResponses[404],
|
||||||
|
429: ErrorResponses[429],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
validator(
|
||||||
|
"json",
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
planType: z.enum(PlanType),
|
||||||
|
successUrl: z.string().url("Success url must be a valid url")
|
||||||
|
})
|
||||||
|
.openapi({
|
||||||
|
description: "Details of the team to create",
|
||||||
|
example: {
|
||||||
|
planType: Examples.Subscription.planType,
|
||||||
|
successUrl: "https://your-url.io/thanks"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const body = c.req.valid("json");
|
||||||
|
const actor = assertActor("user");
|
||||||
|
|
||||||
|
const user = await User.fromID(actor.properties.userID);
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
throw new VisibleError(
|
||||||
|
"not_found",
|
||||||
|
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
|
||||||
|
"User not found",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user.polarCustomerID)
|
||||||
|
throw new VisibleError(
|
||||||
|
"not_found",
|
||||||
|
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
|
||||||
|
"User does not contain Polar customer ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
const checkoutUrl = await Polar.createCheckout({ customerID: user.polarCustomerID, planType: body.planType, successUrl: body.successUrl })
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
data: {
|
||||||
|
checkoutUrl,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.post("/webhook",
|
||||||
|
async (c) => {
|
||||||
|
const requestBody = await c.req.text();
|
||||||
|
|
||||||
|
const webhookSecret = Resource.PolarWebhookSecret.value
|
||||||
|
|
||||||
|
const webhookHeaders = {
|
||||||
|
"webhook-id": c.req.header("webhook-id") ?? "",
|
||||||
|
"webhook-timestamp": c.req.header("webhook-timestamp") ?? "",
|
||||||
|
"webhook-signature": c.req.header("webhook-signature") ?? "",
|
||||||
|
};
|
||||||
|
|
||||||
|
let webhookPayload: ReturnType<typeof validateEvent>;
|
||||||
|
try {
|
||||||
|
webhookPayload = validateEvent(
|
||||||
|
requestBody,
|
||||||
|
webhookHeaders,
|
||||||
|
webhookSecret,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof WebhookVerificationError) {
|
||||||
|
return c.json({ received: false }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Polar.handleWebhook(webhookPayload)
|
||||||
|
|
||||||
|
return c.json({ received: true });
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
46
packages/functions/src/api/subscription.ts
Normal file
46
packages/functions/src/api/subscription.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { notPublic } from "./auth";
|
||||||
|
import { describeRoute } from "hono-openapi";
|
||||||
|
import { Examples } from "@nestri/core/examples";
|
||||||
|
import { assertActor } from "@nestri/core/actor";
|
||||||
|
import { ErrorResponses, Result } from "./common";
|
||||||
|
import { Subscription } from "@nestri/core/subscription/index";
|
||||||
|
|
||||||
|
export namespace SubscriptionApi {
|
||||||
|
export const route = new Hono()
|
||||||
|
.use(notPublic)
|
||||||
|
.get("/",
|
||||||
|
describeRoute({
|
||||||
|
tags: ["Subscription"],
|
||||||
|
summary: "Get user subscriptions",
|
||||||
|
description: "Get all user subscriptions",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: Result(
|
||||||
|
Subscription.Info.array().openapi({
|
||||||
|
description: "All the subscriptions this user has",
|
||||||
|
example: [Examples.Subscription]
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: "All user subscriptions"
|
||||||
|
},
|
||||||
|
400: ErrorResponses[400],
|
||||||
|
404: ErrorResponses[404],
|
||||||
|
429: ErrorResponses[429],
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
const actor = assertActor("user")
|
||||||
|
|
||||||
|
const subscriptions = await Subscription.fromUserID(actor.properties.userID)
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
data: subscriptions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,9 +5,12 @@ import { describeRoute } from "hono-openapi";
|
|||||||
import { User } from "@nestri/core/user/index";
|
import { User } from "@nestri/core/user/index";
|
||||||
import { Team } from "@nestri/core/team/index";
|
import { Team } from "@nestri/core/team/index";
|
||||||
import { Examples } from "@nestri/core/examples";
|
import { Examples } from "@nestri/core/examples";
|
||||||
|
import { Polar } from "@nestri/core/polar/index";
|
||||||
import { Member } from "@nestri/core/member/index";
|
import { Member } from "@nestri/core/member/index";
|
||||||
import { assertActor, withActor } from "@nestri/core/actor";
|
import { assertActor, withActor } from "@nestri/core/actor";
|
||||||
import { ErrorResponses, Result, validator } from "./common";
|
import { ErrorResponses, Result, validator } from "./common";
|
||||||
|
import { Subscription } from "@nestri/core/subscription/index";
|
||||||
|
import { PlanType } from "@nestri/core/subscription/subscription.sql";
|
||||||
|
|
||||||
export namespace TeamApi {
|
export namespace TeamApi {
|
||||||
export const route = new Hono()
|
export const route = new Hono()
|
||||||
@@ -49,7 +52,12 @@ export namespace TeamApi {
|
|||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: Result(
|
schema: Result(
|
||||||
z.literal("ok")
|
z.object({
|
||||||
|
checkoutUrl: z.string().openapi({
|
||||||
|
description: "The checkout url to confirm subscription for this team",
|
||||||
|
example: "https://polar.sh/checkout/2903038439320298377"
|
||||||
|
})
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -63,17 +71,24 @@ export namespace TeamApi {
|
|||||||
}),
|
}),
|
||||||
validator(
|
validator(
|
||||||
"json",
|
"json",
|
||||||
Team.create.schema.omit({ id: true }).openapi({
|
Team.create.schema
|
||||||
|
.pick({ slug: true, name: true })
|
||||||
|
.extend({ planType: z.enum(PlanType), successUrl: z.string().url("Success url must be a valid url") })
|
||||||
|
.openapi({
|
||||||
description: "Details of the team to create",
|
description: "Details of the team to create",
|
||||||
//@ts-expect-error
|
example: {
|
||||||
example: { ...Examples.Team, id: undefined }
|
slug: Examples.Team.slug,
|
||||||
|
name: Examples.Team.name,
|
||||||
|
planType: Examples.Subscription.planType,
|
||||||
|
successUrl: "https://your-url.io/thanks"
|
||||||
|
},
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const body = c.req.valid("json")
|
const body = c.req.valid("json")
|
||||||
const actor = assertActor("user");
|
const actor = assertActor("user");
|
||||||
|
|
||||||
const teamID = await Team.create(body);
|
const teamID = await Team.create({ name: body.name, slug: body.slug });
|
||||||
|
|
||||||
await withActor(
|
await withActor(
|
||||||
{
|
{
|
||||||
@@ -82,14 +97,28 @@ export namespace TeamApi {
|
|||||||
teamID,
|
teamID,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
() =>
|
async () => {
|
||||||
Member.create({
|
await Member.create({
|
||||||
first: true,
|
first: true,
|
||||||
email: actor.properties.email,
|
email: actor.properties.email,
|
||||||
}),
|
});
|
||||||
|
|
||||||
|
await Subscription.create({
|
||||||
|
planType: body.planType,
|
||||||
|
userID: actor.properties.userID,
|
||||||
|
// FIXME: Make this make sense
|
||||||
|
tokens: body.planType === "free" ? 100 : body.planType === "pro" ? 1000 : body.planType === "family" ? 10000 : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return c.json({ data: "ok" })
|
const checkoutUrl = await Polar.createCheckout({ planType: body.planType, successUrl: body.successUrl, teamID })
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
data: {
|
||||||
|
checkoutUrl,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
24
packages/functions/sst-env.d.ts
vendored
24
packages/functions/sst-env.d.ts
vendored
@@ -57,10 +57,34 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"NestriFamilyMonthly": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"NestriFamilyYearly": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"NestriFreeMonthly": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"NestriProMonthly": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"NestriProYearly": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"PolarSecret": {
|
"PolarSecret": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"PolarWebhookSecret": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"Realtime": {
|
"Realtime": {
|
||||||
"authorizer": string
|
"authorizer": string
|
||||||
"endpoint": string
|
"endpoint": string
|
||||||
|
|||||||
0
packages/scripts/src/db-reset.sh
Executable file → Normal file
0
packages/scripts/src/db-reset.sh
Executable file → Normal file
0
packages/scripts/src/psql.sh
Executable file → Normal file
0
packages/scripts/src/psql.sh
Executable file → Normal file
@@ -8,8 +8,8 @@ import { useNavigate } from "@solidjs/router";
|
|||||||
import { useOpenAuth } from "@openauthjs/solid";
|
import { useOpenAuth } from "@openauthjs/solid";
|
||||||
import { utility } from "@nestri/www/ui/utility";
|
import { utility } from "@nestri/www/ui/utility";
|
||||||
import { useAccount } from "../providers/account";
|
import { useAccount } from "../providers/account";
|
||||||
import { Container, Screen as FullScreen } from "@nestri/www/ui/layout";
|
|
||||||
import { FormField, Input, Select } from "@nestri/www/ui/form";
|
import { FormField, Input, Select } from "@nestri/www/ui/form";
|
||||||
|
import { Container, Screen as FullScreen } from "@nestri/www/ui/layout";
|
||||||
import { createForm, getValue, setError, valiForm } from "@modular-forms/solid";
|
import { createForm, getValue, setError, valiForm } from "@modular-forms/solid";
|
||||||
|
|
||||||
const nameRegex = /^[a-z0-9\-]+$/
|
const nameRegex = /^[a-z0-9\-]+$/
|
||||||
@@ -32,8 +32,9 @@ const Hr = styled("hr", {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const Plan = {
|
const Plan = {
|
||||||
Pro: 'BYOG',
|
Free: 'free',
|
||||||
Basic: 'Hosted',
|
Pro: 'pro',
|
||||||
|
Family: 'family',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const schema = v.object({
|
const schema = v.object({
|
||||||
@@ -110,6 +111,13 @@ const UrlTitle = styled("span", {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a form for creating a new team with validated fields for team name, slug, and plan type.
|
||||||
|
*
|
||||||
|
* Submits the form data to the API to create the team, displays validation errors, and navigates to the new team's page upon success.
|
||||||
|
*
|
||||||
|
* @remark If the chosen team slug is already taken, an error message is shown for the slug field.
|
||||||
|
*/
|
||||||
export function CreateTeamComponent() {
|
export function CreateTeamComponent() {
|
||||||
const [form, { Form, Field }] = createForm({
|
const [form, { Form, Field }] = createForm({
|
||||||
validate: valiForm(schema),
|
validate: valiForm(schema),
|
||||||
@@ -215,12 +223,14 @@ export function CreateTeamComponent() {
|
|||||||
required
|
required
|
||||||
value={field.value}
|
value={field.value}
|
||||||
badges={[
|
badges={[
|
||||||
{ label: "BYOG", color: "purple" },
|
{ label: "Free", color: "gray" },
|
||||||
{ label: "Hosted", color: "blue" },
|
{ label: "Pro", color: "blue" },
|
||||||
|
{ label: "Family", color: "purple" },
|
||||||
]}
|
]}
|
||||||
options={[
|
options={[
|
||||||
{ label: "I'll be playing on my machine", value: 'BYOG' },
|
{ label: "I'll be playing by myself", value: 'free' },
|
||||||
{ label: "I'll be playing on the cloud", value: 'Hosted' },
|
{ label: "I'll be playing with 3 friends", value: 'pro' },
|
||||||
|
{ label: "I'll be playing with 5 family members", value: 'family' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|||||||
@@ -201,15 +201,12 @@ const Nav = styled("nav", {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the application's header, featuring navigation, branding, and team details.
|
* Displays the application's fixed top navigation bar with branding, team information, and navigation links.
|
||||||
*
|
*
|
||||||
* This component displays a navigation bar that includes the logo, team avatar, team name, a badge
|
* The header includes the app logo, team avatar and name, a badge indicating the team's plan type, and navigation links related to the team. The header's appearance updates dynamically based on the user's scroll position.
|
||||||
* reflecting the team's plan type, and navigation links. It adjusts its styling based on the scroll
|
|
||||||
* position by toggling visual effects on the navigation wrapper. A scroll event listener is added
|
|
||||||
* on mount to update the header's appearance when the user scrolls and is removed on unmount.
|
|
||||||
*
|
*
|
||||||
* @param props.children - Optional child elements rendered below the header component.
|
* @param props.children - Optional elements rendered below the header.
|
||||||
* @returns The header component element.
|
* @returns The rendered header component.
|
||||||
*/
|
*/
|
||||||
export function Header(props: ParentProps) {
|
export function Header(props: ParentProps) {
|
||||||
// const team = useContext(TeamContext)
|
// const team = useContext(TeamContext)
|
||||||
@@ -218,7 +215,7 @@ export function Header(props: ParentProps) {
|
|||||||
id: "tea_01JPACSPYWTTJ66F32X3AWWFWE",
|
id: "tea_01JPACSPYWTTJ66F32X3AWWFWE",
|
||||||
slug: "wanjohiryan",
|
slug: "wanjohiryan",
|
||||||
name: "Wanjohi",
|
name: "Wanjohi",
|
||||||
planType: "BYOG"
|
planType: "Pro"
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -233,6 +230,7 @@ export function Header(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// const account = useAccount()
|
// const account = useAccount()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<NavWrapper scrolled={hasScrolled()}>
|
<NavWrapper scrolled={hasScrolled()}>
|
||||||
@@ -294,14 +292,14 @@ export function Header(props: ParentProps) {
|
|||||||
/>
|
/>
|
||||||
<TeamLabel style={{ color: theme.color.d1000.gray }}>{team!().name}</TeamLabel>
|
<TeamLabel style={{ color: theme.color.d1000.gray }}>{team!().name}</TeamLabel>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={team!().planType === "BYOG"}>
|
<Match when={team!().planType === "Family"}>
|
||||||
<Badge style={{ "background-color": theme.color.purple.d700 }}>
|
<Badge style={{ "background-color": theme.color.purple.d700 }}>
|
||||||
<span style={{ "line-height": 0 }} >BYOG</span>
|
<span style={{ "line-height": 0 }} >Family</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={team!().planType === "Hosted"}>
|
<Match when={team!().planType === "Pro"}>
|
||||||
<Badge style={{ "background-color": theme.color.blue.d700 }}>
|
<Badge style={{ "background-color": theme.color.blue.d700 }}>
|
||||||
<span style={{ "line-height": 0 }}>Hosted</span>
|
<span style={{ "line-height": 0 }}>Pro</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const TeamRoute = (
|
|||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={!team()}>
|
<Match when={!team()}>
|
||||||
TODO: Add a public page for (other) teams
|
{/* TODO: Add a public page for (other) teams */}
|
||||||
<NotAllowed header />
|
<NotAllowed header />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={team()}>
|
<Match when={team()}>
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export const InputRadio = styled("input", {
|
|||||||
const Label = styled("p", {
|
const Label = styled("p", {
|
||||||
base: {
|
base: {
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
|
textAlign: "left",
|
||||||
letterSpacing: -0.1,
|
letterSpacing: -0.1,
|
||||||
fontSize: theme.font.size.mono_sm,
|
fontSize: theme.font.size.mono_sm,
|
||||||
textTransform: "capitalize",
|
textTransform: "capitalize",
|
||||||
@@ -213,6 +214,7 @@ const Hint = styled("p", {
|
|||||||
fontSize: theme.font.size.xs,
|
fontSize: theme.font.size.xs,
|
||||||
lineHeight: theme.font.lineHeight,
|
lineHeight: theme.font.lineHeight,
|
||||||
color: theme.color.gray.d800,
|
color: theme.color.gray.d800,
|
||||||
|
textAlign: "left"
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
color: {
|
color: {
|
||||||
|
|||||||
24
sst-env.d.ts
vendored
24
sst-env.d.ts
vendored
@@ -57,10 +57,34 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"NestriFamilyMonthly": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"NestriFamilyYearly": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"NestriFreeMonthly": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"NestriProMonthly": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"NestriProYearly": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"PolarSecret": {
|
"PolarSecret": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"PolarWebhookSecret": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"Realtime": {
|
"Realtime": {
|
||||||
"authorizer": string
|
"authorizer": string
|
||||||
"endpoint": string
|
"endpoint": string
|
||||||
|
|||||||
Reference in New Issue
Block a user