@yeyo11/shipcast 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -2
  3. package/dist/cli/shipcast.js +25240 -0
  4. package/dist/fonts/Fraunces-Roman.ttf +0 -0
  5. package/dist/fonts/Fraunces-WonkItalic.ttf +0 -0
  6. package/dist/fonts/InstrumentSans-Regular.ttf +0 -0
  7. package/dist/fonts/InstrumentSans-SemiBold.ttf +0 -0
  8. package/dist/fonts/OFL-Fraunces.txt +93 -0
  9. package/dist/fonts/OFL-InstrumentSans.txt +93 -0
  10. package/dist/fonts/README.md +14 -0
  11. package/drizzle/0000_registry.sql +54 -0
  12. package/drizzle/0001_post_model.sql +144 -0
  13. package/drizzle/0002_api_tokens.sql +9 -0
  14. package/drizzle/0003_jobs.sql +20 -0
  15. package/drizzle/0004_account_snapshots.sql +15 -0
  16. package/drizzle/0006_comments.sql +29 -0
  17. package/drizzle/0007_comments_paging.sql +2 -0
  18. package/drizzle/0008_assistant_sessions.sql +25 -0
  19. package/drizzle/0009_attention_dismissals.sql +4 -0
  20. package/drizzle/0010_comments_search.sql +4 -0
  21. package/drizzle/0011_launches_list.sql +3 -0
  22. package/drizzle/0012_telemetry.sql +40 -0
  23. package/drizzle/0013_publish_attempts.sql +18 -0
  24. package/drizzle/0014_secrets.sql +8 -0
  25. package/drizzle/0015_draft_snapshots.sql +13 -0
  26. package/drizzle/0016_metrics_checked_at.sql +1 -0
  27. package/drizzle/0017_metric_days.sql +25 -0
  28. package/drizzle/0018_launch_model.sql +124 -0
  29. package/drizzle/0019_launch_imports.sql +20 -0
  30. package/drizzle/0020_launch_deletions.sql +15 -0
  31. package/drizzle/0021_dedupe_key_backfill.sql +22 -0
  32. package/drizzle/0022_drop_draft_snapshots.sql +4 -0
  33. package/drizzle/0023_push_subscriptions.sql +14 -0
  34. package/drizzle/meta/0000_snapshot.json +400 -0
  35. package/drizzle/meta/0001_snapshot.json +537 -0
  36. package/drizzle/meta/0002_snapshot.json +604 -0
  37. package/drizzle/meta/0003_snapshot.json +732 -0
  38. package/drizzle/meta/0004_snapshot.json +816 -0
  39. package/drizzle/meta/0006_snapshot.json +1036 -0
  40. package/drizzle/meta/0007_snapshot.json +1084 -0
  41. package/drizzle/meta/0008_snapshot.json +1298 -0
  42. package/drizzle/meta/0009_snapshot.json +1324 -0
  43. package/drizzle/meta/0010_snapshot.json +1372 -0
  44. package/drizzle/meta/0011_snapshot.json +1448 -0
  45. package/drizzle/meta/0012_snapshot.json +1717 -0
  46. package/drizzle/meta/0013_snapshot.json +1844 -0
  47. package/drizzle/meta/0014_snapshot.json +1896 -0
  48. package/drizzle/meta/0015_snapshot.json +1978 -0
  49. package/drizzle/meta/0016_snapshot.json +1984 -0
  50. package/drizzle/meta/0017_snapshot.json +2112 -0
  51. package/drizzle/meta/0018_snapshot.json +3051 -0
  52. package/drizzle/meta/0019_snapshot.json +3198 -0
  53. package/drizzle/meta/0020_snapshot.json +3315 -0
  54. package/drizzle/meta/0021_snapshot.json +3315 -0
  55. package/drizzle/meta/0022_snapshot.json +3233 -0
  56. package/drizzle/meta/0023_snapshot.json +3333 -0
  57. package/drizzle/meta/_journal.json +167 -0
  58. package/package.json +62 -4
Binary file
@@ -0,0 +1,93 @@
1
+ Copyright 2020 The Fraunces Project Authors (github.com/undercasetype/Fraunces) Fraunces-Italic[SOFT,WONK,opsz,wght].ttf: Copyright 2020 The Fraunces Project Authors (github.com/undercasetype/Fraunces)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,93 @@
1
+ Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans) InstrumentSans-Italic[wdth,wght].ttf: Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,14 @@
1
+ # Fonts for the launch image
2
+
3
+ Static instances of the variable fonts the personal site uses, because Satori reads neither variable
4
+ fonts nor WOFF2. Latin subset, cut from the Fontsource variable files with
5
+ `fonttools varLib.instancer`:
6
+
7
+ | File | Source | Axes |
8
+ | --- | --- | --- |
9
+ | `Fraunces-Roman.ttf` | Fraunces, full, normal | `wght=400 opsz=144 SOFT=30 WONK=1` (the site's headlines) |
10
+ | `Fraunces-WonkItalic.ttf` | Fraunces, full, italic | `wght=400 opsz=144 SOFT=100 WONK=1` (each headline word's first letter) |
11
+ | `InstrumentSans-Regular.ttf` | Instrument Sans, standard | `wght=400 wdth=100` |
12
+ | `InstrumentSans-SemiBold.ttf` | Instrument Sans, standard | `wght=600 wdth=100` |
13
+
14
+ Both families are under the SIL Open Font License 1.1 (`OFL-*.txt`), with no reserved font name.
@@ -0,0 +1,54 @@
1
+ CREATE TABLE "accounts" (
2
+ "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "accounts_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
3
+ "network" text NOT NULL,
4
+ "brand" text NOT NULL,
5
+ "label" text NOT NULL,
6
+ "settings" text DEFAULT '{}' NOT NULL,
7
+ "is_primary" integer DEFAULT 0 NOT NULL,
8
+ "status" text DEFAULT 'unchecked' NOT NULL,
9
+ "identity" text,
10
+ "problem" text,
11
+ "checked_at" text,
12
+ "created_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL,
13
+ CONSTRAINT "accounts_network_brand_label_unique" UNIQUE("network","brand","label")
14
+ );
15
+ --> statement-breakpoint
16
+ CREATE TABLE "cursors" (
17
+ "key" text PRIMARY KEY NOT NULL,
18
+ "value" text NOT NULL
19
+ );
20
+ --> statement-breakpoint
21
+ CREATE TABLE "launches" (
22
+ "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "launches_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
23
+ "repo" text NOT NULL,
24
+ "ref" text NOT NULL,
25
+ "trigger" text NOT NULL,
26
+ "kind" text NOT NULL,
27
+ "brand" text NOT NULL,
28
+ "status" text NOT NULL,
29
+ "detail" text,
30
+ "created_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL,
31
+ "updated_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL,
32
+ CONSTRAINT "launches_repo_ref_unique" UNIQUE("repo","ref")
33
+ );
34
+ --> statement-breakpoint
35
+ CREATE TABLE "metrics" (
36
+ "launch_id" integer NOT NULL,
37
+ "source" text NOT NULL,
38
+ "name" text NOT NULL,
39
+ "value" double precision NOT NULL,
40
+ "taken_at" text NOT NULL
41
+ );
42
+ --> statement-breakpoint
43
+ CREATE TABLE "posts" (
44
+ "launch_id" integer NOT NULL,
45
+ "channel" text NOT NULL,
46
+ "url" text,
47
+ "external_id" text,
48
+ "published_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL,
49
+ CONSTRAINT "posts_launch_id_channel_pk" PRIMARY KEY("launch_id","channel")
50
+ );
51
+ --> statement-breakpoint
52
+ ALTER TABLE "metrics" ADD CONSTRAINT "metrics_launch_id_launches_id_fk" FOREIGN KEY ("launch_id") REFERENCES "public"."launches"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
53
+ ALTER TABLE "posts" ADD CONSTRAINT "posts_launch_id_launches_id_fk" FOREIGN KEY ("launch_id") REFERENCES "public"."launches"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
54
+ CREATE INDEX "metrics_by_launch" ON "metrics" USING btree ("launch_id","source","name","taken_at");
@@ -0,0 +1,144 @@
1
+ -- One row per published piece, typed settings, and the conversion of what an install already holds.
2
+ -- Drizzle's migrator runs a migration in one transaction, so a conversion that fails leaves the
3
+ -- registry as it was; every insert also skips what is already there, so a rerun of a statement
4
+ -- converts nothing twice. See docs/decisions/post-model.md
5
+ CREATE TABLE "settings" (
6
+ "key" text PRIMARY KEY NOT NULL,
7
+ "value" jsonb NOT NULL,
8
+ "updated_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL
9
+ );
10
+ --> statement-breakpoint
11
+ ALTER TABLE "posts" DROP CONSTRAINT "posts_launch_id_channel_pk";--> statement-breakpoint
12
+ ALTER TABLE "metrics" ADD COLUMN "post_id" integer;--> statement-breakpoint
13
+ ALTER TABLE "posts" ADD COLUMN "id" integer PRIMARY KEY NOT NULL GENERATED BY DEFAULT AS IDENTITY (sequence name "posts_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1);--> statement-breakpoint
14
+ ALTER TABLE "posts" ADD COLUMN "account_id" integer;--> statement-breakpoint
15
+ -- Null until the conversion below gives every old row its kind: a null kind marks an old row
16
+ ALTER TABLE "posts" ADD COLUMN "kind" text;--> statement-breakpoint
17
+ ALTER TABLE "posts" ADD COLUMN "status" text DEFAULT 'live' NOT NULL;--> statement-breakpoint
18
+ ALTER TABLE "posts" ADD COLUMN "content_hash" text;--> statement-breakpoint
19
+ ALTER TABLE "posts" ADD COLUMN "scheduled_at" text;--> statement-breakpoint
20
+ ALTER TABLE "posts" ADD COLUMN "updated_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL;--> statement-breakpoint
21
+ ALTER TABLE "metrics" ADD CONSTRAINT "metrics_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
22
+ ALTER TABLE "posts" ADD CONSTRAINT "posts_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
23
+ CREATE INDEX "metrics_by_post" ON "metrics" USING btree ("post_id");--> statement-breakpoint
24
+ -- Before the conversion, so its inserts can skip a piece already recorded. The old rows were one per
25
+ -- launch and channel, so with a null kind and no account they already satisfy it
26
+ ALTER TABLE "posts" ADD CONSTRAINT "posts_piece_unique" UNIQUE NULLS NOT DISTINCT("launch_id","channel","account_id","kind");--> statement-breakpoint
27
+ UPDATE "posts" SET "updated_at" = "published_at";--> statement-breakpoint
28
+
29
+ -- The blog: one article per language from `en:<slug>,es:<slug>`, at <origin of the old URL>/<lang>/<slug>/
30
+ INSERT INTO "posts" ("launch_id", "channel", "account_id", "kind", "external_id", "url", "status", "published_at", "updated_at")
31
+ SELECT p."launch_id", 'blog', NULL, 'article:' || m.part[1], m.part[2],
32
+ substring(p."url" from '^[a-z]+://[^/]+') || '/' || m.part[1] || '/' || m.part[2] || '/',
33
+ 'live', p."published_at", p."published_at"
34
+ FROM "posts" p
35
+ CROSS JOIN LATERAL regexp_matches(p."external_id", '(?:^|,)(en|es):([^,]+)', 'g') AS m(part)
36
+ WHERE p."channel" = 'blog' AND p."kind" IS NULL AND substring(p."url" from '^[a-z]+://[^/]+') IS NOT NULL
37
+ ON CONFLICT DO NOTHING;--> statement-breakpoint
38
+ -- An older blog row with one slug for both languages: its URL's language, and the other one at the
39
+ -- same slug, which is what the metrics assumed of it
40
+ INSERT INTO "posts" ("launch_id", "channel", "account_id", "kind", "external_id", "url", "status", "published_at", "updated_at")
41
+ SELECT p."launch_id", 'blog', NULL, 'article:' || v.lang, substring(v.url from '/([^/]+)/?$'), v.url, 'live', p."published_at", p."published_at"
42
+ FROM "posts" p
43
+ CROSS JOIN LATERAL (
44
+ VALUES (COALESCE(substring(p."url" from '^[a-z]+://[^/]+/(en|es)/'), 'en'), p."url"),
45
+ (CASE WHEN p."url" ~ '^[a-z]+://[^/]+/en/' THEN 'es' END, regexp_replace(p."url", '^([a-z]+://[^/]+)/en/', '\1/es/'))
46
+ ) AS v(lang, url)
47
+ WHERE p."channel" = 'blog' AND p."kind" IS NULL AND p."url" IS NOT NULL AND v.lang IS NOT NULL
48
+ AND COALESCE(p."external_id", '') !~ '(^|,)(en|es):'
49
+ ON CONFLICT DO NOTHING;--> statement-breakpoint
50
+ DELETE FROM "posts" p
51
+ WHERE p."channel" = 'blog' AND p."kind" IS NULL
52
+ AND EXISTS (SELECT 1 FROM "posts" a WHERE a."launch_id" = p."launch_id" AND a."channel" = 'blog' AND a."kind" LIKE 'article:%');--> statement-breakpoint
53
+
54
+ -- The newsletter: one issue per language, from the `newsletter-sent:<launch>:<lang>` cursors, which
55
+ -- hold each campaign's id and report URL, and which also record an issue whose sibling never went out
56
+ WITH "sent" AS (
57
+ SELECT split_part(c."key", ':', 2) AS launch, split_part(c."key", ':', 3) AS lang,
58
+ CASE WHEN c."value" IS JSON OBJECT THEN c."value"::jsonb END AS issue
59
+ FROM "cursors" c
60
+ WHERE c."key" ~ '^newsletter-sent:[0-9]+:(en|es)$'
61
+ )
62
+ INSERT INTO "posts" ("launch_id", "channel", "account_id", "kind", "external_id", "url", "status", "published_at", "updated_at")
63
+ SELECT l."id", 'newsletter',
64
+ (SELECT a."id" FROM "accounts" a WHERE a."network" = 'brevo' AND a."brand" = l."brand" AND a."is_primary" = 1 ORDER BY a."id" LIMIT 1),
65
+ 'issue:' || s.lang, s.issue ->> 'externalId', s.issue ->> 'url', 'live',
66
+ COALESCE(p."published_at", to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')),
67
+ COALESCE(p."published_at", to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'))
68
+ FROM "sent" s
69
+ JOIN "launches" l ON l."id"::text = s.launch
70
+ LEFT JOIN "posts" p ON p."launch_id" = l."id" AND p."channel" = 'newsletter' AND p."kind" IS NULL
71
+ WHERE s.issue ->> 'externalId' IS NOT NULL
72
+ ON CONFLICT DO NOTHING;--> statement-breakpoint
73
+ -- A newsletter row with no cursors left: its comma-joined ids in the order they were sent, English first
74
+ INSERT INTO "posts" ("launch_id", "channel", "account_id", "kind", "external_id", "url", "status", "published_at", "updated_at")
75
+ SELECT p."launch_id", 'newsletter',
76
+ (SELECT a."id" FROM "accounts" a JOIN "launches" l ON l."id" = p."launch_id"
77
+ WHERE a."network" = 'brevo' AND a."brand" = l."brand" AND a."is_primary" = 1 ORDER BY a."id" LIMIT 1),
78
+ 'issue:' || (ARRAY['en', 'es'])[ids.n], ids.id,
79
+ CASE WHEN p."url" LIKE '%/' || split_part(p."external_id", ',', 1) THEN regexp_replace(p."url", '[^/]+$', ids.id)
80
+ WHEN ids.n = 1 THEN p."url" END,
81
+ 'live', p."published_at", p."published_at"
82
+ FROM "posts" p
83
+ CROSS JOIN LATERAL unnest(string_to_array(p."external_id", ',')) WITH ORDINALITY AS ids(id, n)
84
+ WHERE p."channel" = 'newsletter' AND p."kind" IS NULL AND ids.n <= 2 AND ids.id <> ''
85
+ AND NOT EXISTS (SELECT 1 FROM "posts" i WHERE i."launch_id" = p."launch_id" AND i."channel" = 'newsletter' AND i."kind" LIKE 'issue:%')
86
+ ON CONFLICT DO NOTHING;--> statement-breakpoint
87
+ DELETE FROM "posts" p
88
+ WHERE p."channel" = 'newsletter' AND p."kind" IS NULL
89
+ AND EXISTS (SELECT 1 FROM "posts" i WHERE i."launch_id" = p."launch_id" AND i."channel" = 'newsletter' AND i."kind" LIKE 'issue:%');--> statement-breakpoint
90
+
91
+ -- Instagram's story and comment, kept in `instagram-story:<launch>` and `instagram-comment:<launch>`,
92
+ -- dated as the post they followed
93
+ WITH "extra" AS (
94
+ SELECT split_part(c."key", ':', 2) AS launch, substring(c."key" from '^instagram-(story|comment):') AS what, c."value"
95
+ FROM "cursors" c
96
+ WHERE c."key" ~ '^instagram-(story|comment):[0-9]+$'
97
+ )
98
+ INSERT INTO "posts" ("launch_id", "channel", "account_id", "kind", "external_id", "url", "status", "published_at", "updated_at")
99
+ SELECT l."id", 'instagram',
100
+ (SELECT a."id" FROM "accounts" a WHERE a."network" = 'instagram' AND a."brand" = l."brand" AND a."is_primary" = 1 ORDER BY a."id" LIMIT 1),
101
+ e.what, e."value", NULL, 'live',
102
+ COALESCE(p."published_at", to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')),
103
+ COALESCE(p."published_at", to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'))
104
+ FROM "extra" e
105
+ JOIN "launches" l ON l."id"::text = e.launch
106
+ LEFT JOIN "posts" p ON p."launch_id" = l."id" AND p."channel" = 'instagram' AND (p."kind" IS NULL OR p."kind" = 'post')
107
+ ON CONFLICT DO NOTHING;--> statement-breakpoint
108
+
109
+ -- Every other old row is a post, by the account that was primary for its network and brand
110
+ UPDATE "posts" p
111
+ SET "kind" = 'post',
112
+ "account_id" = (
113
+ SELECT a."id" FROM "accounts" a JOIN "launches" l ON l."id" = p."launch_id"
114
+ WHERE a."network" = CASE p."channel" WHEN 'newsletter' THEN 'brevo' ELSE p."channel" END
115
+ AND a."brand" = l."brand" AND a."is_primary" = 1 AND p."channel" <> 'blog'
116
+ ORDER BY a."id" LIMIT 1
117
+ )
118
+ WHERE p."kind" IS NULL;--> statement-breakpoint
119
+ ALTER TABLE "posts" ALTER COLUMN "kind" SET NOT NULL;--> statement-breakpoint
120
+
121
+ -- A channel's readings are its post's. The old newsletter readings stay with the launch: each is the
122
+ -- sum of both issues, so neither issue is theirs alone
123
+ UPDATE "metrics" m SET "post_id" = p."id"
124
+ FROM "posts" p
125
+ WHERE m."post_id" IS NULL AND p."launch_id" = m."launch_id" AND p."channel" = m."source" AND p."kind" = 'post'
126
+ AND m."source" NOT IN ('blog', 'newsletter');--> statement-breakpoint
127
+
128
+ -- The owner's automation choices, out of the cursors
129
+ INSERT INTO "settings" ("key", "value")
130
+ SELECT c."key", to_jsonb(c."value") FROM "cursors" c WHERE c."key" = 'automation:mode'
131
+ ON CONFLICT DO NOTHING;--> statement-breakpoint
132
+ INSERT INTO "settings" ("key", "value")
133
+ SELECT c."key", to_jsonb(c."value"::numeric) FROM "cursors" c WHERE c."key" = 'automation:interval' AND c."value" ~ '^[0-9]{1,9}$'
134
+ ON CONFLICT DO NOTHING;--> statement-breakpoint
135
+
136
+ -- The cursors that became rows or settings, now that they are copied; anything not copied stays
137
+ DELETE FROM "cursors" c
138
+ WHERE (c."key" ~ '^newsletter-sent:[0-9]+:(en|es)$' AND EXISTS (
139
+ SELECT 1 FROM "posts" i WHERE i."launch_id"::text = split_part(c."key", ':', 2) AND i."channel" = 'newsletter'
140
+ AND i."kind" = 'issue:' || split_part(c."key", ':', 3)))
141
+ OR (c."key" ~ '^instagram-(story|comment):[0-9]+$' AND EXISTS (
142
+ SELECT 1 FROM "posts" x WHERE x."launch_id"::text = split_part(c."key", ':', 2) AND x."channel" = 'instagram'
143
+ AND x."kind" = substring(c."key" from '^instagram-(story|comment):')))
144
+ OR (c."key" IN ('automation:mode', 'automation:interval') AND EXISTS (SELECT 1 FROM "settings" s WHERE s."key" = c."key"));
@@ -0,0 +1,9 @@
1
+ CREATE TABLE "api_tokens" (
2
+ "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "api_tokens_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
3
+ "name" text NOT NULL,
4
+ "hash" text NOT NULL,
5
+ "scopes" text[] NOT NULL,
6
+ "created_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL,
7
+ "last_used_at" text,
8
+ "revoked_at" text
9
+ );
@@ -0,0 +1,20 @@
1
+ -- What the app shows of the queue's work: one row per pg-boss job, kept after it ends and across
2
+ -- restarts. pg-boss creates its own schema (pgboss) when it starts; nothing here touches it. See
3
+ -- docs/decisions/job-queue.md
4
+ CREATE TABLE "jobs" (
5
+ "id" text PRIMARY KEY NOT NULL,
6
+ "queue" text NOT NULL,
7
+ "title" text NOT NULL,
8
+ "launch_id" integer,
9
+ "state" text DEFAULT 'created' NOT NULL,
10
+ "attempts" integer DEFAULT 0 NOT NULL,
11
+ "log" jsonb DEFAULT '[]'::jsonb NOT NULL,
12
+ "link" text,
13
+ "created_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL,
14
+ "started_at" text,
15
+ "ended_at" text
16
+ );
17
+ --> statement-breakpoint
18
+ ALTER TABLE "jobs" ADD CONSTRAINT "jobs_launch_id_launches_id_fk" FOREIGN KEY ("launch_id") REFERENCES "public"."launches"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
19
+ CREATE INDEX "jobs_by_created" ON "jobs" USING btree ("created_at");--> statement-breakpoint
20
+ CREATE INDEX "jobs_by_launch" ON "jobs" USING btree ("launch_id");
@@ -0,0 +1,15 @@
1
+ -- Each account's followers, following and posts, one row a day from now on, and the picture the
2
+ -- last check found. Only adds: no existing row changes. See docs/decisions/account-snapshots.md
3
+ CREATE TABLE "account_snapshots" (
4
+ "account_id" integer NOT NULL,
5
+ "day" text NOT NULL,
6
+ "taken_at" text NOT NULL,
7
+ "followers" integer,
8
+ "following" integer,
9
+ "posts" integer,
10
+ "extra" jsonb DEFAULT '{}'::jsonb NOT NULL,
11
+ CONSTRAINT "account_snapshots_account_day" PRIMARY KEY("account_id","day")
12
+ );
13
+ --> statement-breakpoint
14
+ ALTER TABLE "accounts" ADD COLUMN "avatar_url" text;--> statement-breakpoint
15
+ ALTER TABLE "account_snapshots" ADD CONSTRAINT "account_snapshots_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;
@@ -0,0 +1,29 @@
1
+ CREATE TABLE "comments" (
2
+ "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "comments_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
3
+ "post_id" integer NOT NULL,
4
+ "channel" text NOT NULL,
5
+ "external_id" text NOT NULL,
6
+ "author_handle" text NOT NULL,
7
+ "author_name" text,
8
+ "author_avatar_url" text,
9
+ "text" text NOT NULL,
10
+ "url" text,
11
+ "created_at" text NOT NULL,
12
+ "in_reply_to" text,
13
+ "own" boolean DEFAULT false NOT NULL,
14
+ "fetched_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL,
15
+ "status" text DEFAULT 'new' NOT NULL,
16
+ "classification" text,
17
+ "draft" text,
18
+ "note" text,
19
+ "problem" text,
20
+ "sent_external_id" text,
21
+ "sent_url" text,
22
+ "sent_at" text,
23
+ "updated_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL,
24
+ CONSTRAINT "comments_channel_external_unique" UNIQUE("channel","external_id")
25
+ );
26
+ --> statement-breakpoint
27
+ ALTER TABLE "comments" ADD CONSTRAINT "comments_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
28
+ CREATE INDEX "comments_by_post" ON "comments" USING btree ("post_id");--> statement-breakpoint
29
+ CREATE INDEX "comments_by_status" ON "comments" USING btree ("status","created_at");
@@ -0,0 +1,2 @@
1
+ CREATE INDEX "comments_by_channel" ON "comments" USING btree ("channel","created_at" DESC NULLS LAST,"id" DESC NULLS LAST);--> statement-breakpoint
2
+ CREATE INDEX "comments_by_newest" ON "comments" USING btree ("created_at" DESC NULLS LAST,"id" DESC NULLS LAST);
@@ -0,0 +1,25 @@
1
+ CREATE TABLE "assistant_messages" (
2
+ "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "assistant_messages_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
3
+ "session_id" integer NOT NULL,
4
+ "role" text NOT NULL,
5
+ "content" text NOT NULL,
6
+ "proposals" jsonb DEFAULT '[]'::jsonb NOT NULL,
7
+ "selection" jsonb,
8
+ "problem" text,
9
+ "created_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL
10
+ );
11
+ --> statement-breakpoint
12
+ CREATE TABLE "assistant_sessions" (
13
+ "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "assistant_sessions_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
14
+ "title" text NOT NULL,
15
+ "launch_id" integer,
16
+ "draft_path" text,
17
+ "draft_label" text,
18
+ "created_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL,
19
+ "updated_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL
20
+ );
21
+ --> statement-breakpoint
22
+ ALTER TABLE "assistant_messages" ADD CONSTRAINT "assistant_messages_session_id_assistant_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."assistant_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
23
+ ALTER TABLE "assistant_sessions" ADD CONSTRAINT "assistant_sessions_launch_id_launches_id_fk" FOREIGN KEY ("launch_id") REFERENCES "public"."launches"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
24
+ CREATE INDEX "assistant_messages_by_session" ON "assistant_messages" USING btree ("session_id","id");--> statement-breakpoint
25
+ CREATE INDEX "assistant_sessions_by_updated" ON "assistant_sessions" USING btree ("updated_at","id");
@@ -0,0 +1,4 @@
1
+ CREATE TABLE "attention_dismissals" (
2
+ "key" text PRIMARY KEY NOT NULL,
3
+ "dismissed_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL
4
+ );
@@ -0,0 +1,4 @@
1
+ CREATE EXTENSION IF NOT EXISTS pg_trgm;--> statement-breakpoint
2
+ CREATE INDEX IF NOT EXISTS "comments_text_trgm" ON "comments" USING gin ("text" gin_trgm_ops);--> statement-breakpoint
3
+ CREATE INDEX IF NOT EXISTS "comments_author_handle_trgm" ON "comments" USING gin ("author_handle" gin_trgm_ops);--> statement-breakpoint
4
+ CREATE INDEX IF NOT EXISTS "comments_author_name_trgm" ON "comments" USING gin ("author_name" gin_trgm_ops);
@@ -0,0 +1,3 @@
1
+ CREATE INDEX "launches_by_newest" ON "launches" USING btree ("created_at" DESC NULLS LAST,"id" DESC NULLS LAST);--> statement-breakpoint
2
+ CREATE INDEX "launches_by_status" ON "launches" USING btree ("status","created_at" DESC NULLS LAST,"id" DESC NULLS LAST);--> statement-breakpoint
3
+ CREATE INDEX "launches_by_repo" ON "launches" USING btree ("repo","created_at" DESC NULLS LAST,"id" DESC NULLS LAST);
@@ -0,0 +1,40 @@
1
+ CREATE TABLE "checks" (
2
+ "id" integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "checks_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
3
+ "taken_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL,
4
+ "name" text NOT NULL,
5
+ "ok" boolean NOT NULL,
6
+ "value" double precision NOT NULL,
7
+ "detail" jsonb DEFAULT '{}'::jsonb NOT NULL
8
+ );
9
+ --> statement-breakpoint
10
+ CREATE TABLE "provider_calls" (
11
+ "hour" text NOT NULL,
12
+ "process" text NOT NULL,
13
+ "origin" text NOT NULL,
14
+ "provider" text NOT NULL,
15
+ "method" text NOT NULL,
16
+ "endpoint" text NOT NULL,
17
+ "status" integer NOT NULL,
18
+ "calls" integer NOT NULL,
19
+ "total_ms" double precision NOT NULL,
20
+ "bytes" double precision DEFAULT 0 NOT NULL,
21
+ "latency" jsonb DEFAULT '{}'::jsonb NOT NULL,
22
+ "quota_peak" double precision,
23
+ "quota_limit" integer,
24
+ CONSTRAINT "provider_calls_key" PRIMARY KEY("hour","process","origin","provider","method","endpoint","status")
25
+ );
26
+ --> statement-breakpoint
27
+ CREATE TABLE "timings" (
28
+ "hour" text NOT NULL,
29
+ "process" text NOT NULL,
30
+ "kind" text NOT NULL,
31
+ "name" text NOT NULL,
32
+ "outcome" text NOT NULL,
33
+ "count" integer NOT NULL,
34
+ "total_ms" double precision NOT NULL,
35
+ "max_ms" double precision NOT NULL,
36
+ "latency" jsonb DEFAULT '{}'::jsonb NOT NULL,
37
+ CONSTRAINT "timings_key" PRIMARY KEY("hour","process","kind","name","outcome")
38
+ );
39
+ --> statement-breakpoint
40
+ CREATE INDEX "checks_by_name" ON "checks" USING btree ("name","taken_at");
@@ -0,0 +1,18 @@
1
+ CREATE TABLE "publish_attempts" (
2
+ "launch_id" integer NOT NULL,
3
+ "channel" text NOT NULL,
4
+ "kind" text NOT NULL,
5
+ "state" text NOT NULL,
6
+ "attempts" integer DEFAULT 1 NOT NULL,
7
+ "lease_until" text,
8
+ "holder" text,
9
+ "error" text,
10
+ "external_id" text,
11
+ "url" text,
12
+ "started_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL,
13
+ "updated_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL,
14
+ CONSTRAINT "publish_attempts_piece" PRIMARY KEY("launch_id","channel","kind")
15
+ );
16
+ --> statement-breakpoint
17
+ ALTER TABLE "publish_attempts" ADD CONSTRAINT "publish_attempts_launch_id_launches_id_fk" FOREIGN KEY ("launch_id") REFERENCES "public"."launches"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
18
+ CREATE INDEX "publish_attempts_by_state" ON "publish_attempts" USING btree ("state");
@@ -0,0 +1,8 @@
1
+ CREATE TABLE "secrets" (
2
+ "key" text PRIMARY KEY NOT NULL,
3
+ "label" text DEFAULT '' NOT NULL,
4
+ "sealed" text NOT NULL,
5
+ "sealed_key" text NOT NULL,
6
+ "version" integer DEFAULT 1 NOT NULL,
7
+ "updated_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL
8
+ );
@@ -0,0 +1,13 @@
1
+ CREATE TABLE "draft_snapshots" (
2
+ "launch_id" integer PRIMARY KEY NOT NULL,
3
+ "pr_number" integer NOT NULL,
4
+ "url" text NOT NULL,
5
+ "branch" text NOT NULL,
6
+ "head_sha" text,
7
+ "state" text NOT NULL,
8
+ "launch_yml" text,
9
+ "files" jsonb NOT NULL,
10
+ "read_at" text DEFAULT to_char(statement_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') NOT NULL
11
+ );
12
+ --> statement-breakpoint
13
+ ALTER TABLE "draft_snapshots" ADD CONSTRAINT "draft_snapshots_launch_id_launches_id_fk" FOREIGN KEY ("launch_id") REFERENCES "public"."launches"("id") ON DELETE no action ON UPDATE no action;
@@ -0,0 +1 @@
1
+ ALTER TABLE "metrics" ADD COLUMN "checked_at" text;