spotters: activity (per-country score) and seen (collection) tables for cars, planes, trains, wildlife
All checks were successful
Deploy production / deploy (push) Successful in 1m7s
All checks were successful
Deploy production / deploy (push) Successful in 1m7s
This commit is contained in:
106
drizzle/20261025130829_spotters/migration.sql
Normal file
106
drizzle/20261025130829_spotters/migration.sql
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
CREATE TABLE "cars"."activity" (
|
||||||
|
"model_id" integer,
|
||||||
|
"area" text,
|
||||||
|
"score" real NOT NULL,
|
||||||
|
CONSTRAINT "activity_pkey" PRIMARY KEY("area","model_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "cars"."seen" (
|
||||||
|
"model_id" integer PRIMARY KEY,
|
||||||
|
"seen_on" date,
|
||||||
|
"note" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "planes"."activity" (
|
||||||
|
"type_id" integer,
|
||||||
|
"area" text,
|
||||||
|
"score" real NOT NULL,
|
||||||
|
CONSTRAINT "activity_pkey" PRIMARY KEY("area","type_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "planes"."seen" (
|
||||||
|
"type_id" integer PRIMARY KEY,
|
||||||
|
"seen_on" date,
|
||||||
|
"note" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "wildlife"."activity" (
|
||||||
|
"species_id" integer,
|
||||||
|
"area" text,
|
||||||
|
"score" real NOT NULL,
|
||||||
|
CONSTRAINT "activity_pkey" PRIMARY KEY("area","species_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "wildlife"."seen" (
|
||||||
|
"species_id" integer PRIMARY KEY,
|
||||||
|
"seen_on" date,
|
||||||
|
"note" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "trains"."activity" (
|
||||||
|
"type_id" integer,
|
||||||
|
"area" text,
|
||||||
|
"score" real NOT NULL,
|
||||||
|
CONSTRAINT "activity_pkey" PRIMARY KEY("area","type_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "trains"."seen" (
|
||||||
|
"type_id" integer PRIMARY KEY,
|
||||||
|
"seen_on" date,
|
||||||
|
"note" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "activity_area_score_idx" ON "cars"."activity" ("area","score");--> statement-breakpoint
|
||||||
|
CREATE INDEX "activity_area_score_idx" ON "planes"."activity" ("area","score");--> statement-breakpoint
|
||||||
|
CREATE INDEX "activity_area_score_idx" ON "wildlife"."activity" ("area","score");--> statement-breakpoint
|
||||||
|
CREATE INDEX "activity_area_score_idx" ON "trains"."activity" ("area","score");--> statement-breakpoint
|
||||||
|
ALTER TABLE "cars"."activity" ADD CONSTRAINT "activity_model_id_models_id_fkey" FOREIGN KEY ("model_id") REFERENCES "cars"."models"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "cars"."seen" ADD CONSTRAINT "seen_model_id_models_id_fkey" FOREIGN KEY ("model_id") REFERENCES "cars"."models"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "planes"."activity" ADD CONSTRAINT "activity_type_id_types_id_fkey" FOREIGN KEY ("type_id") REFERENCES "planes"."types"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "planes"."seen" ADD CONSTRAINT "seen_type_id_types_id_fkey" FOREIGN KEY ("type_id") REFERENCES "planes"."types"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "wildlife"."activity" ADD CONSTRAINT "activity_species_id_species_id_fkey" FOREIGN KEY ("species_id") REFERENCES "wildlife"."species"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "wildlife"."seen" ADD CONSTRAINT "seen_species_id_species_id_fkey" FOREIGN KEY ("species_id") REFERENCES "wildlife"."species"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "trains"."activity" ADD CONSTRAINT "activity_type_id_types_id_fkey" FOREIGN KEY ("type_id") REFERENCES "trains"."types"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
ALTER TABLE "trains"."seen" ADD CONSTRAINT "seen_type_id_types_id_fkey" FOREIGN KEY ("type_id") REFERENCES "trains"."types"("id") ON DELETE CASCADE;--> statement-breakpoint
|
||||||
|
-- Hand-written: updated_at triggers, grants, GUI registration. activity is filled by
|
||||||
|
-- scripts/spotters-activity.ts; seen is written by the owner from the pokedex pages.
|
||||||
|
DO $$
|
||||||
|
DECLARE s text;
|
||||||
|
BEGIN
|
||||||
|
FOREACH s IN ARRAY ARRAY['cars','planes','trains','wildlife'] LOOP
|
||||||
|
EXECUTE format('CREATE TRIGGER touch_updated_at BEFORE UPDATE ON %I.seen FOR EACH ROW EXECUTE FUNCTION meta.touch_updated_at()', s);
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'personal_app') THEN
|
||||||
|
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON %I.activity, %I.seen TO personal_app', s, s);
|
||||||
|
END IF;
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'personal_ro') THEN
|
||||||
|
EXECUTE format('GRANT SELECT ON %I.activity, %I.seen TO personal_ro', s, s);
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF to_regclass('meta.tables') IS NOT NULL THEN
|
||||||
|
INSERT INTO meta.tables (schema_name, table_name, slug, display_name, kind, editable, has_done, sort_order)
|
||||||
|
VALUES ('cars','activity','cars-activity','Car activity by country','core',false,false,1000),
|
||||||
|
('cars','seen','cars-seen','Cars seen','core',false,false,1000),
|
||||||
|
('planes','activity','planes-activity','Plane activity by country','core',false,false,1000),
|
||||||
|
('planes','seen','planes-seen','Planes seen','core',false,false,1000),
|
||||||
|
('trains','activity','trains-activity','Train activity by country','core',false,false,1000),
|
||||||
|
('trains','seen','trains-seen','Trains seen','core',false,false,1000),
|
||||||
|
('wildlife','activity','wildlife-activity','Wildlife activity by country','core',false,false,1000),
|
||||||
|
('wildlife','seen','wildlife-seen','Wildlife seen','core',false,false,1000)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
IF has_function_privilege('meta.sync(integer)', 'EXECUTE') THEN
|
||||||
|
PERFORM meta.sync();
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
26143
drizzle/20261025130829_spotters/snapshot.json
Normal file
26143
drizzle/20261025130829_spotters/snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,9 @@ import {
|
|||||||
smallint,
|
smallint,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
uniqueIndex
|
uniqueIndex,
|
||||||
|
date,
|
||||||
|
primaryKey
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
import { carsSchema } from './schemas';
|
import { carsSchema } from './schemas';
|
||||||
import { id, timestamps } from './helpers';
|
import { id, timestamps } from './helpers';
|
||||||
@@ -178,3 +180,34 @@ export const carPhotos = carsSchema.table(
|
|||||||
},
|
},
|
||||||
(t) => [uniqueIndex('photos_model_position_key').on(t.modelId, t.position)]
|
(t) => [uniqueIndex('photos_model_position_key').on(t.modelId, t.position)]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How active each entry is per country or region (scripts/spotters-activity.ts), so the
|
||||||
|
* dex can be sorted by "most active in NL / Germany / Europe ..." without live joins.
|
||||||
|
* `area` is a lowercase ISO 3166-1 alpha-2 code ("nl") or a region code ("europe",
|
||||||
|
* see src/lib/spotting/areas.ts). Missing row = no known activity there.
|
||||||
|
*/
|
||||||
|
export const carActivity = carsSchema.table(
|
||||||
|
'activity',
|
||||||
|
{
|
||||||
|
modelId: integer('model_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => carModels.id, { onDelete: 'cascade' }),
|
||||||
|
area: text('area').notNull(),
|
||||||
|
score: real('score').notNull()
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
primaryKey({ columns: [t.area, t.modelId] }),
|
||||||
|
index('activity_area_score_idx').on(t.area, t.score)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Stan's collection: entries he has seen / collected (owner-only writes, public reads). */
|
||||||
|
export const carSeen = carsSchema.table('seen', {
|
||||||
|
modelId: integer('model_id')
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => carModels.id, { onDelete: 'cascade' }),
|
||||||
|
seenOn: date('seen_on'),
|
||||||
|
note: text('note'),
|
||||||
|
...timestamps
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import {
|
|||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
uniqueIndex,
|
uniqueIndex,
|
||||||
type AnyPgColumn
|
type AnyPgColumn,
|
||||||
|
date,
|
||||||
|
primaryKey
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
import { planesSchema } from './schemas';
|
import { planesSchema } from './schemas';
|
||||||
import { id, timestamps } from './helpers';
|
import { id, timestamps } from './helpers';
|
||||||
@@ -155,3 +157,34 @@ export const planePhotos = planesSchema.table(
|
|||||||
},
|
},
|
||||||
(t) => [uniqueIndex('photos_type_position_key').on(t.typeId, t.position)]
|
(t) => [uniqueIndex('photos_type_position_key').on(t.typeId, t.position)]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How active each entry is per country or region (scripts/spotters-activity.ts), so the
|
||||||
|
* dex can be sorted by "most active in NL / Germany / Europe ..." without live joins.
|
||||||
|
* `area` is a lowercase ISO 3166-1 alpha-2 code ("nl") or a region code ("europe",
|
||||||
|
* see src/lib/spotting/areas.ts). Missing row = no known activity there.
|
||||||
|
*/
|
||||||
|
export const planeActivity = planesSchema.table(
|
||||||
|
'activity',
|
||||||
|
{
|
||||||
|
typeId: integer('type_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => planeTypes.id, { onDelete: 'cascade' }),
|
||||||
|
area: text('area').notNull(),
|
||||||
|
score: real('score').notNull()
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
primaryKey({ columns: [t.area, t.typeId] }),
|
||||||
|
index('activity_area_score_idx').on(t.area, t.score)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Stan's collection: entries he has seen / collected (owner-only writes, public reads). */
|
||||||
|
export const planeSeen = planesSchema.table('seen', {
|
||||||
|
typeId: integer('type_id')
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => planeTypes.id, { onDelete: 'cascade' }),
|
||||||
|
seenOn: date('seen_on'),
|
||||||
|
note: text('note'),
|
||||||
|
...timestamps
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
uniqueIndex,
|
uniqueIndex,
|
||||||
type AnyPgColumn
|
type AnyPgColumn,
|
||||||
|
date
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
import { trainsSchema } from './schemas';
|
import { trainsSchema } from './schemas';
|
||||||
import { id, timestamps } from './helpers';
|
import { id, timestamps } from './helpers';
|
||||||
@@ -185,3 +186,34 @@ export const trainPhotos = trainsSchema.table(
|
|||||||
},
|
},
|
||||||
(t) => [uniqueIndex('photos_type_position_key').on(t.typeId, t.position)]
|
(t) => [uniqueIndex('photos_type_position_key').on(t.typeId, t.position)]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How active each entry is per country or region (scripts/spotters-activity.ts), so the
|
||||||
|
* dex can be sorted by "most active in NL / Germany / Europe ..." without live joins.
|
||||||
|
* `area` is a lowercase ISO 3166-1 alpha-2 code ("nl") or a region code ("europe",
|
||||||
|
* see src/lib/spotting/areas.ts). Missing row = no known activity there.
|
||||||
|
*/
|
||||||
|
export const trainActivity = trainsSchema.table(
|
||||||
|
'activity',
|
||||||
|
{
|
||||||
|
typeId: integer('type_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => trainTypes.id, { onDelete: 'cascade' }),
|
||||||
|
area: text('area').notNull(),
|
||||||
|
score: real('score').notNull()
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
primaryKey({ columns: [t.area, t.typeId] }),
|
||||||
|
index('activity_area_score_idx').on(t.area, t.score)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Stan's collection: entries he has seen / collected (owner-only writes, public reads). */
|
||||||
|
export const trainSeen = trainsSchema.table('seen', {
|
||||||
|
typeId: integer('type_id')
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => trainTypes.id, { onDelete: 'cascade' }),
|
||||||
|
seenOn: date('seen_on'),
|
||||||
|
note: text('note'),
|
||||||
|
...timestamps
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import {
|
|||||||
smallint,
|
smallint,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
uniqueIndex
|
uniqueIndex,
|
||||||
|
date,
|
||||||
|
primaryKey
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
import { wildlifeSchema } from './schemas';
|
import { wildlifeSchema } from './schemas';
|
||||||
import { id, timestamps } from './helpers';
|
import { id, timestamps } from './helpers';
|
||||||
@@ -133,3 +135,34 @@ export const wildlifePhotos = wildlifeSchema.table(
|
|||||||
uniqueIndex('photos_species_photo_key').on(t.speciesId, t.photoId)
|
uniqueIndex('photos_species_photo_key').on(t.speciesId, t.photoId)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How active each entry is per country or region (scripts/spotters-activity.ts), so the
|
||||||
|
* dex can be sorted by "most active in NL / Germany / Europe ..." without live joins.
|
||||||
|
* `area` is a lowercase ISO 3166-1 alpha-2 code ("nl") or a region code ("europe",
|
||||||
|
* see src/lib/spotting/areas.ts). Missing row = no known activity there.
|
||||||
|
*/
|
||||||
|
export const wildlifeActivity = wildlifeSchema.table(
|
||||||
|
'activity',
|
||||||
|
{
|
||||||
|
speciesId: integer('species_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => wildlifeSpecies.id, { onDelete: 'cascade' }),
|
||||||
|
area: text('area').notNull(),
|
||||||
|
score: real('score').notNull()
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
primaryKey({ columns: [t.area, t.speciesId] }),
|
||||||
|
index('activity_area_score_idx').on(t.area, t.score)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Stan's collection: entries he has seen / collected (owner-only writes, public reads). */
|
||||||
|
export const wildlifeSeen = wildlifeSchema.table('seen', {
|
||||||
|
speciesId: integer('species_id')
|
||||||
|
.primaryKey()
|
||||||
|
.references(() => wildlifeSpecies.id, { onDelete: 'cascade' }),
|
||||||
|
seenOn: date('seen_on'),
|
||||||
|
note: text('note'),
|
||||||
|
...timestamps
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user