@rocapine/community 0.3.3 → 0.6.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.
- package/README.md +24 -18
- package/lib/commands/init.d.ts +1 -2
- package/lib/commands/init.js +11 -23
- package/lib/commands/upgrade.d.ts +0 -2
- package/lib/commands/upgrade.js +9 -31
- package/lib/index.js +6 -6
- package/lib/install-shared.d.ts +15 -17
- package/lib/install-shared.js +19 -51
- package/package.json +1 -1
- package/templates/migrations/core/000_settings.sql +128 -0
- package/templates/migrations/core/003_moderation.sql +4 -65
- package/templates/migrations/core/008_webhooks.sql +27 -0
- package/templates/migrations/push/002_triggers.sql +4 -84
- package/templates/migrations/push/003_webhooks.sql +50 -0
- package/templates/migrations/reaction/001_reactions.sql +3 -54
- package/templates/migrations/reaction/002_webhooks.sql +24 -0
- package/templates/migrations/translation/001_translations.sql +3 -82
- package/templates/migrations/translation/002_webhooks.sql +48 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ version. The binary is named `community`.
|
|
|
13
13
|
## `init`
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx @rocapine/community init [--modules core,push,polls,reaction,inbox] [--project-url <url>] [--anon-key <key>] [--dir supabase]
|
|
16
|
+
npx @rocapine/community init [--modules core,push,polls,reaction,inbox,translation] [--project-url <url>] [--anon-key <key>] [--dir supabase]
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Copies the migrations and Edge Functions for the requested modules into
|
|
@@ -21,13 +21,13 @@ Copies the migrations and Edge Functions for the requested modules into
|
|
|
21
21
|
relative to the current directory), and writes a `community-sdk.json`
|
|
22
22
|
manifest at the repo root recording what was installed.
|
|
23
23
|
|
|
24
|
-
- `--modules` — comma-separated (`core,push,polls,reaction,inbox`). `core`
|
|
24
|
+
- `--modules` — comma-separated (`core,push,polls,reaction,inbox,translation`). `core`
|
|
25
25
|
is always implied even if omitted. Omit the flag entirely to install every
|
|
26
26
|
module. Regardless of the order you type them, modules are installed
|
|
27
|
-
`core → push → polls → reaction → inbox` — this order is load-bearing:
|
|
27
|
+
`core → push → polls → reaction → inbox → translation` — this order is load-bearing:
|
|
28
28
|
the inbox module's reaction trigger is guarded at install time and must
|
|
29
29
|
find the reaction module's table already present, so reaction has to land
|
|
30
|
-
first. Requesting `inbox` without `reaction` prints a (non-fatal) warning.
|
|
30
|
+
first. Requesting `inbox` without `reaction`, or `translation` without `polls`, prints a (non-fatal) warning (reactions missing from the inbox; poll labels left untranslated).
|
|
31
31
|
- `--project-url` / `--anon-key` — needed only if a migration in the
|
|
32
32
|
selected modules carries a placeholder (see "Placeholder guard" below).
|
|
33
33
|
If omitted, the CLI tries to read the project URL from
|
|
@@ -107,17 +107,23 @@ the _current_ template set and points at `docs/compat.md` to check/adjust
|
|
|
107
107
|
is what `upgrade` diffs against (timestamped destination filenames can't
|
|
108
108
|
be compared meaningfully run to run).
|
|
109
109
|
|
|
110
|
-
##
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
110
|
+
## Project settings (no value is ever baked into a migration)
|
|
111
|
+
|
|
112
|
+
The pg_cron jobs and pg_net webhooks that call back into the project's own
|
|
113
|
+
Edge Functions read the project URL and anon key from Supabase Vault, through
|
|
114
|
+
two private helpers defined by `core/000_settings.sql`
|
|
115
|
+
(`community_project_url()` / `community_anon_key()`). The migration files are
|
|
116
|
+
therefore identical for every project: the same `supabase/` folder deploys to
|
|
117
|
+
a sandbox and to production. What differs is one statement, run once per
|
|
118
|
+
project after `db push`:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
supabase db query --linked "select public.community_settings_set('https://<ref>.supabase.co', '<anon key>');"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`init`/`upgrade` print it (filled in when `--project-url` / `--anon-key` are
|
|
125
|
+
passed, placeholders otherwise). Until a project is seeded, every webhook
|
|
126
|
+
logs a warning instead of calling out — inserts are never blocked — and the
|
|
127
|
+
daily sweeps catch up once it is. The anon key is public by design (it ships
|
|
128
|
+
inside the app binary): the Edge Functions do privileged work through their
|
|
129
|
+
own service-role env, the JWT only passes `verify_jwt`.
|
package/lib/commands/init.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare const ALREADY_INITIALIZED_MESSAGE = "community-sdk: already initi
|
|
|
5
5
|
export interface InitOptions {
|
|
6
6
|
/** Modules to install. Defaults to every module. `core` is always implied. */
|
|
7
7
|
modules?: string[];
|
|
8
|
+
/** Optional: only used to print the settings seed statement ready to run. */
|
|
8
9
|
projectUrl?: string;
|
|
9
10
|
anonKey?: string;
|
|
10
11
|
/** Target Supabase directory, relative to `cwd`. Defaults to "supabase". */
|
|
@@ -13,8 +14,6 @@ export interface InitOptions {
|
|
|
13
14
|
cwd?: string;
|
|
14
15
|
/** Source of the migrations/ + functions/ trees. Defaults to the shipped templates. */
|
|
15
16
|
templatesDir?: string;
|
|
16
|
-
/** Used to ask for projectUrl/anonKey when missing and not derivable. */
|
|
17
|
-
prompt?: (question: string) => Promise<string>;
|
|
18
17
|
/** Called for non-fatal dependency warnings (e.g. inbox without reaction). */
|
|
19
18
|
onWarn?: (message: string) => void;
|
|
20
19
|
/** Used to print the summary + next steps. */
|
package/lib/commands/init.js
CHANGED
|
@@ -37,7 +37,6 @@ exports.ALREADY_INITIALIZED_MESSAGE = exports.MODULE_ORDER = void 0;
|
|
|
37
37
|
exports.runInit = runInit;
|
|
38
38
|
const fs = __importStar(require("node:fs"));
|
|
39
39
|
const path = __importStar(require("node:path"));
|
|
40
|
-
const substitute_1 = require("../substitute");
|
|
41
40
|
const manifest_1 = require("../manifest");
|
|
42
41
|
const install_shared_1 = require("../install-shared");
|
|
43
42
|
Object.defineProperty(exports, "MODULE_ORDER", { enumerable: true, get: function () { return install_shared_1.MODULE_ORDER; } });
|
|
@@ -54,17 +53,10 @@ async function runInit(options = {}) {
|
|
|
54
53
|
const dir = (0, install_shared_1.resolveContainedDir)(cwd, dirRel);
|
|
55
54
|
const templatesDir = options.templatesDir ?? (0, install_shared_1.defaultTemplatesDir)();
|
|
56
55
|
const { migrationsSrcRoot, functionsSrcRoot } = (0, install_shared_1.resolveTemplateRoots)(templatesDir);
|
|
57
|
-
const { projectUrl, anonKey } = await (0, install_shared_1.resolveProjectCredentials)({
|
|
58
|
-
dir,
|
|
59
|
-
projectUrl: options.projectUrl,
|
|
60
|
-
anonKey: options.anonKey,
|
|
61
|
-
prompt: options.prompt,
|
|
62
|
-
});
|
|
63
56
|
const now = options.now ?? new Date();
|
|
64
57
|
// ---- migrations + functions, staged atomically: on any failure partway
|
|
65
|
-
// through, every file this run wrote
|
|
66
|
-
//
|
|
67
|
-
// retry starts clean instead of finding stale/duplicate files and no
|
|
58
|
+
// through, every file this run wrote is removed before the error
|
|
59
|
+
// propagates, so a retry starts clean instead of finding stale/duplicate files and no
|
|
68
60
|
// manifest to explain them. The manifest itself is written last, only once
|
|
69
61
|
// every file below has landed successfully.
|
|
70
62
|
const migrationsDestRoot = path.join(dir, "migrations");
|
|
@@ -89,16 +81,8 @@ async function runInit(options = {}) {
|
|
|
89
81
|
fileIndex += 1;
|
|
90
82
|
const destFilename = (0, install_shared_1.migrationDestFilename)(timestamp, moduleName, name);
|
|
91
83
|
const rawSql = fs.readFileSync(path.join(moduleSrcDir, filename), "utf8");
|
|
92
|
-
let substituted;
|
|
93
|
-
try {
|
|
94
|
-
substituted = (0, substitute_1.substitutePlaceholders)(rawSql, { projectUrl, anonKey });
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
98
|
-
throw new Error(`${message} (source: ${moduleName}/${filename})`);
|
|
99
|
-
}
|
|
100
84
|
const destPath = path.join(migrationsDestRoot, destFilename);
|
|
101
|
-
fs.writeFileSync(destPath,
|
|
85
|
+
fs.writeFileSync(destPath, rawSql, "utf8");
|
|
102
86
|
writtenPaths.push(destPath);
|
|
103
87
|
installedTemplates.push((0, manifest_1.migrationTemplateId)(moduleName, name));
|
|
104
88
|
}
|
|
@@ -135,15 +119,19 @@ async function runInit(options = {}) {
|
|
|
135
119
|
installedTemplates: installedTemplates.sort(),
|
|
136
120
|
};
|
|
137
121
|
(0, manifest_1.writeManifest)(cwd, manifest);
|
|
138
|
-
printNextSteps(log, canonicalModules
|
|
122
|
+
printNextSteps(log, canonicalModules, {
|
|
123
|
+
projectUrl: options.projectUrl,
|
|
124
|
+
anonKey: options.anonKey,
|
|
125
|
+
});
|
|
139
126
|
return { manifest, dir };
|
|
140
127
|
}
|
|
141
|
-
function printNextSteps(log, modules) {
|
|
128
|
+
function printNextSteps(log, modules, values) {
|
|
142
129
|
log("");
|
|
143
130
|
log(`community-sdk initialized (${modules.join(", ")}).`);
|
|
144
131
|
log("Next steps:");
|
|
145
132
|
log(" 1. Review the copied migrations, then: supabase db push");
|
|
146
|
-
|
|
133
|
+
(0, install_shared_1.printSettingsSeedStep)(log, values, 2);
|
|
134
|
+
log(" 3. Set Edge Function secrets: supabase secrets set OPENAI_API_KEY=...");
|
|
147
135
|
log(" optional: SLACK_WEBHOOK_URL, COMMUNITY_APP_NAME, COMMUNITY_FALLBACK_NAME");
|
|
148
136
|
if (modules.includes("push")) {
|
|
149
137
|
log(" push module: EXPO_ACCESS_TOKEN (only if your Expo project enforces Enhanced Security for push); COMMUNITY_PUSH_COPY (optional per-locale copy)");
|
|
@@ -151,5 +139,5 @@ function printNextSteps(log, modules) {
|
|
|
151
139
|
if (modules.includes("translation")) {
|
|
152
140
|
log(' translation module: COMMUNITY_TRANSLATION_LOCALES="en,es-419,..." (required, same list as modules.translation.locales in the app); optional: COMMUNITY_TRANSLATION_MODEL, COMMUNITY_TRANSLATION_STYLE');
|
|
153
141
|
}
|
|
154
|
-
log("
|
|
142
|
+
log(" 4. Deploy the Edge Functions: supabase functions deploy");
|
|
155
143
|
}
|
|
@@ -11,8 +11,6 @@ export interface UpgradeOptions {
|
|
|
11
11
|
templatesDir?: string;
|
|
12
12
|
/** Module names to install on top of what's already in the manifest — validated, ordered and merged via resolveModules, same rules as `init --modules` (unknown names throw, dependency gaps warn). */
|
|
13
13
|
addModules?: string[];
|
|
14
|
-
/** Used to ask for projectUrl/anonKey when a newly copied migration needs them. */
|
|
15
|
-
prompt?: (question: string) => Promise<string>;
|
|
16
14
|
/** Called for non-fatal warnings — most importantly, functions overwritten with new content. */
|
|
17
15
|
onWarn?: (message: string) => void;
|
|
18
16
|
/** Used to print the summary + next steps. */
|
package/lib/commands/upgrade.js
CHANGED
|
@@ -38,7 +38,6 @@ exports.runUpgrade = runUpgrade;
|
|
|
38
38
|
const fs = __importStar(require("node:fs"));
|
|
39
39
|
const os = __importStar(require("node:os"));
|
|
40
40
|
const path = __importStar(require("node:path"));
|
|
41
|
-
const substitute_1 = require("../substitute");
|
|
42
41
|
const manifest_1 = require("../manifest");
|
|
43
42
|
const install_shared_1 = require("../install-shared");
|
|
44
43
|
exports.NOT_INITIALIZED_MESSAGE = "community-sdk: not initialized, run init (or adopt)";
|
|
@@ -112,20 +111,6 @@ async function runUpgrade(options = {}) {
|
|
|
112
111
|
overwrittenFunctions: [],
|
|
113
112
|
};
|
|
114
113
|
}
|
|
115
|
-
// ---- credentials, only if a pending migration actually needs them ----
|
|
116
|
-
const needsCredentials = pending.some((p) => p.rawSql.includes("__SUPABASE"));
|
|
117
|
-
let projectUrl;
|
|
118
|
-
let anonKey;
|
|
119
|
-
if (needsCredentials) {
|
|
120
|
-
const creds = await (0, install_shared_1.resolveProjectCredentials)({
|
|
121
|
-
dir,
|
|
122
|
-
projectUrl: options.projectUrl,
|
|
123
|
-
anonKey: options.anonKey,
|
|
124
|
-
prompt: options.prompt,
|
|
125
|
-
});
|
|
126
|
-
projectUrl = creds.projectUrl;
|
|
127
|
-
anonKey = creds.anonKey;
|
|
128
|
-
}
|
|
129
114
|
const migrationsDestRoot = path.join(dir, "migrations");
|
|
130
115
|
const writtenMigrationPaths = [];
|
|
131
116
|
const addedMigrations = [];
|
|
@@ -133,23 +118,8 @@ async function runUpgrade(options = {}) {
|
|
|
133
118
|
const createdFunctionDirs = [];
|
|
134
119
|
const backups = [];
|
|
135
120
|
try {
|
|
136
|
-
// ---- prepare every migration's final content up front: a placeholder
|
|
137
|
-
// error here must throw before a single byte is written to disk. ----
|
|
138
121
|
const now = options.now ?? new Date();
|
|
139
|
-
const prepared = pending.map((p) => {
|
|
140
|
-
if (!p.rawSql.includes("__SUPABASE"))
|
|
141
|
-
return { ...p, content: p.rawSql };
|
|
142
|
-
try {
|
|
143
|
-
return {
|
|
144
|
-
...p,
|
|
145
|
-
content: (0, substitute_1.substitutePlaceholders)(p.rawSql, { projectUrl: projectUrl, anonKey: anonKey }),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
catch (err) {
|
|
149
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
150
|
-
throw new Error(`${message} (source: ${p.moduleName}/${p.templateFilename})`);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
122
|
+
const prepared = pending.map((p) => ({ ...p, content: p.rawSql }));
|
|
153
123
|
if (prepared.length > 0) {
|
|
154
124
|
fs.mkdirSync(migrationsDestRoot, { recursive: true });
|
|
155
125
|
prepared.forEach((p, index) => {
|
|
@@ -225,6 +195,7 @@ async function runUpgrade(options = {}) {
|
|
|
225
195
|
};
|
|
226
196
|
(0, manifest_1.writeManifest)(cwd, updatedManifest);
|
|
227
197
|
printSummary(log, onWarn, {
|
|
198
|
+
values: { projectUrl: options.projectUrl, anonKey: options.anonKey },
|
|
228
199
|
addedModules,
|
|
229
200
|
addedMigrations,
|
|
230
201
|
newFunctions,
|
|
@@ -296,6 +267,13 @@ function printSummary(log, onWarn, summary) {
|
|
|
296
267
|
if (summary.addedMigrations.length > 0) {
|
|
297
268
|
log(" 1. Review the new migrations, then: supabase db push");
|
|
298
269
|
}
|
|
270
|
+
if (summary.addedMigrations.some((f) => /_community_core_settings\.sql$/.test(f))) {
|
|
271
|
+
// First upgrade onto the Vault-backed settings: the new *_webhooks
|
|
272
|
+
// migrations read the project URL / anon key from there, so the seed must
|
|
273
|
+
// follow this push (until then every webhook logs a warning and the
|
|
274
|
+
// daily sweeps catch up — nothing is lost, nothing is blocked).
|
|
275
|
+
(0, install_shared_1.printSettingsSeedStep)(log, summary.values, 2);
|
|
276
|
+
}
|
|
299
277
|
if (summary.newFunctions.length > 0 || summary.overwrittenFunctions.length > 0) {
|
|
300
278
|
log(" 2. Review the function changes, then: supabase functions deploy");
|
|
301
279
|
}
|
package/lib/index.js
CHANGED
|
@@ -60,9 +60,9 @@ program
|
|
|
60
60
|
program
|
|
61
61
|
.command("init")
|
|
62
62
|
.description("Copy the community backend into a Supabase project: migrations, Edge Functions, and a community-sdk.json manifest")
|
|
63
|
-
.option("--modules <modules>", "comma-separated module list (core,push,polls,reaction,inbox) — core is always implied; defaults to all modules")
|
|
64
|
-
.option("--project-url <url>", "Supabase project URL, e.g. https://<ref>.supabase.co")
|
|
65
|
-
.option("--anon-key <key>", "Supabase anon key")
|
|
63
|
+
.option("--modules <modules>", "comma-separated module list (core,push,polls,reaction,inbox,translation) — core is always implied; defaults to all modules")
|
|
64
|
+
.option("--project-url <url>", "Supabase project URL, e.g. https://<ref>.supabase.co — optional, only fills in the printed settings seed statement")
|
|
65
|
+
.option("--anon-key <key>", "Supabase anon key — optional, only fills in the printed settings seed statement")
|
|
66
66
|
.option("--dir <dir>", "target Supabase directory, relative to the current directory", "supabase")
|
|
67
67
|
.action(async (opts) => {
|
|
68
68
|
try {
|
|
@@ -82,8 +82,8 @@ program
|
|
|
82
82
|
.command("upgrade")
|
|
83
83
|
.description("Copy any new migrations/Edge Functions shipped since this repo's community-sdk.json was last written, and overwrite changed function code (with a warning)")
|
|
84
84
|
.option("--add-modules <modules>", "comma-separated module list to install on top of what's already in community-sdk.json (e.g. translation)")
|
|
85
|
-
.option("--project-url <url>", "Supabase project URL, e.g. https://<ref>.supabase.co
|
|
86
|
-
.option("--anon-key <key>", "Supabase anon key
|
|
85
|
+
.option("--project-url <url>", "Supabase project URL, e.g. https://<ref>.supabase.co — optional, only fills in the printed settings seed statement")
|
|
86
|
+
.option("--anon-key <key>", "Supabase anon key — optional, only fills in the printed settings seed statement")
|
|
87
87
|
.option("--dir <dir>", "target Supabase directory, relative to the current directory", "supabase")
|
|
88
88
|
.action(async (opts) => {
|
|
89
89
|
try {
|
|
@@ -103,7 +103,7 @@ program
|
|
|
103
103
|
.command("adopt")
|
|
104
104
|
.description("Register an already-live community backend (e.g. an app that hand-rolled its own before this SDK existed) with community-sdk.json, without copying any files")
|
|
105
105
|
.requiredOption("--schema-version <n>", "the community-sdk schema version this app's live backend corresponds to")
|
|
106
|
-
.requiredOption("--modules <modules>", "comma-separated module list (core,push,polls,reaction,inbox) — core is always implied")
|
|
106
|
+
.requiredOption("--modules <modules>", "comma-separated module list (core,push,polls,reaction,inbox,translation) — core is always implied")
|
|
107
107
|
.option("--dir <dir>", "Supabase directory this app would use, relative to the current directory", "supabase")
|
|
108
108
|
.action(async (opts) => {
|
|
109
109
|
try {
|
package/lib/install-shared.d.ts
CHANGED
|
@@ -48,23 +48,6 @@ export declare function resolveTemplateRoots(templatesDir: string): {
|
|
|
48
48
|
functionsSrcRoot: string;
|
|
49
49
|
};
|
|
50
50
|
export declare function readOwnPackageVersion(): string;
|
|
51
|
-
export declare function deriveProjectUrlFromConfig(dir: string): string | null;
|
|
52
|
-
/**
|
|
53
|
-
* Resolves the Supabase project URL + anon key from (in order) explicit
|
|
54
|
-
* flags, `<dir>/config.toml` (URL only), or an interactive prompt — failing
|
|
55
|
-
* loudly instead of hanging when stdin is not a TTY and no `prompt`
|
|
56
|
-
* override was injected. Shared by `init` and `upgrade`, both of which only
|
|
57
|
-
* need this when a placeholder-bearing file is about to be written.
|
|
58
|
-
*/
|
|
59
|
-
export declare function resolveProjectCredentials(options: {
|
|
60
|
-
dir: string;
|
|
61
|
-
projectUrl?: string;
|
|
62
|
-
anonKey?: string;
|
|
63
|
-
prompt?: (question: string) => Promise<string>;
|
|
64
|
-
}): Promise<{
|
|
65
|
-
projectUrl: string;
|
|
66
|
-
anonKey: string;
|
|
67
|
-
}>;
|
|
68
51
|
export declare function formatTimestamp(date: Date): string;
|
|
69
52
|
export declare function addSeconds(date: Date, seconds: number): Date;
|
|
70
53
|
/** Deno test files (`*_test.ts`) stay in the repo: never copied into a host's
|
|
@@ -74,3 +57,18 @@ export declare function listFilesRecursive(dir: string): string[];
|
|
|
74
57
|
export declare function toRelativePosix(base: string, target: string): string;
|
|
75
58
|
/** Resolves `dirRel` against `cwd`, throwing if it would escape the project root. */
|
|
76
59
|
export declare function resolveContainedDir(cwd: string, dirRel: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* The one statement to run once per Supabase project after `db push`: it
|
|
62
|
+
* stores the project's own URL and anon key in Vault, where the pg_cron jobs
|
|
63
|
+
* and pg_net webhooks read them (core/000_settings.sql). The migration files
|
|
64
|
+
* themselves carry no project-specific value, so the same files deploy to a
|
|
65
|
+
* sandbox and a production project.
|
|
66
|
+
*/
|
|
67
|
+
export declare function settingsSeedSql(values?: {
|
|
68
|
+
projectUrl?: string;
|
|
69
|
+
anonKey?: string;
|
|
70
|
+
}): string;
|
|
71
|
+
export declare function printSettingsSeedStep(log: (message: string) => void, values: {
|
|
72
|
+
projectUrl?: string;
|
|
73
|
+
anonKey?: string;
|
|
74
|
+
}, step: number): void;
|
package/lib/install-shared.js
CHANGED
|
@@ -42,17 +42,16 @@ exports.parseMigrationDestFilename = parseMigrationDestFilename;
|
|
|
42
42
|
exports.defaultTemplatesDir = defaultTemplatesDir;
|
|
43
43
|
exports.resolveTemplateRoots = resolveTemplateRoots;
|
|
44
44
|
exports.readOwnPackageVersion = readOwnPackageVersion;
|
|
45
|
-
exports.deriveProjectUrlFromConfig = deriveProjectUrlFromConfig;
|
|
46
|
-
exports.resolveProjectCredentials = resolveProjectCredentials;
|
|
47
45
|
exports.formatTimestamp = formatTimestamp;
|
|
48
46
|
exports.addSeconds = addSeconds;
|
|
49
47
|
exports.isShippedFunctionFile = isShippedFunctionFile;
|
|
50
48
|
exports.listFilesRecursive = listFilesRecursive;
|
|
51
49
|
exports.toRelativePosix = toRelativePosix;
|
|
52
50
|
exports.resolveContainedDir = resolveContainedDir;
|
|
51
|
+
exports.settingsSeedSql = settingsSeedSql;
|
|
52
|
+
exports.printSettingsSeedStep = printSettingsSeedStep;
|
|
53
53
|
const fs = __importStar(require("node:fs"));
|
|
54
54
|
const path = __importStar(require("node:path"));
|
|
55
|
-
const promises_1 = require("node:readline/promises");
|
|
56
55
|
/**
|
|
57
56
|
* Ruled install order (controller ruling, task 18): core -> push -> polls ->
|
|
58
57
|
* reaction -> inbox -> translation. Reaction MUST land before inbox —
|
|
@@ -177,54 +176,6 @@ function readOwnPackageVersion() {
|
|
|
177
176
|
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
178
177
|
return pkg.version;
|
|
179
178
|
}
|
|
180
|
-
function deriveProjectUrlFromConfig(dir) {
|
|
181
|
-
const configPath = path.join(dir, "config.toml");
|
|
182
|
-
if (!fs.existsSync(configPath))
|
|
183
|
-
return null;
|
|
184
|
-
const raw = fs.readFileSync(configPath, "utf8");
|
|
185
|
-
const match = raw.match(/^\s*project_id\s*=\s*"([^"]+)"/m);
|
|
186
|
-
if (!match)
|
|
187
|
-
return null;
|
|
188
|
-
return `https://${match[1]}.supabase.co`;
|
|
189
|
-
}
|
|
190
|
-
function defaultPrompt(question) {
|
|
191
|
-
const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
192
|
-
return rl.question(question).then((answer) => {
|
|
193
|
-
rl.close();
|
|
194
|
-
return answer;
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Resolves the Supabase project URL + anon key from (in order) explicit
|
|
199
|
-
* flags, `<dir>/config.toml` (URL only), or an interactive prompt — failing
|
|
200
|
-
* loudly instead of hanging when stdin is not a TTY and no `prompt`
|
|
201
|
-
* override was injected. Shared by `init` and `upgrade`, both of which only
|
|
202
|
-
* need this when a placeholder-bearing file is about to be written.
|
|
203
|
-
*/
|
|
204
|
-
async function resolveProjectCredentials(options) {
|
|
205
|
-
const usingDefaultPrompt = !options.prompt;
|
|
206
|
-
const prompt = options.prompt ?? defaultPrompt;
|
|
207
|
-
let projectUrl = options.projectUrl;
|
|
208
|
-
let anonKey = options.anonKey;
|
|
209
|
-
if (!projectUrl) {
|
|
210
|
-
projectUrl = deriveProjectUrlFromConfig(options.dir) ?? undefined;
|
|
211
|
-
}
|
|
212
|
-
const stillMissingFlags = [];
|
|
213
|
-
if (!projectUrl)
|
|
214
|
-
stillMissingFlags.push("--project-url");
|
|
215
|
-
if (!anonKey)
|
|
216
|
-
stillMissingFlags.push("--anon-key");
|
|
217
|
-
if (stillMissingFlags.length > 0 && usingDefaultPrompt && !process.stdin.isTTY) {
|
|
218
|
-
throw new Error(`community-sdk: missing required flag(s) ${stillMissingFlags.join(", ")}. stdin is not a TTY, so this command cannot prompt for ${stillMissingFlags.length > 1 ? "them" : "it"} — pass ${stillMissingFlags.length > 1 ? "them" : "it"} explicitly.`);
|
|
219
|
-
}
|
|
220
|
-
if (!projectUrl) {
|
|
221
|
-
projectUrl = (await prompt("Supabase project URL (https://<ref>.supabase.co): ")).trim();
|
|
222
|
-
}
|
|
223
|
-
if (!anonKey) {
|
|
224
|
-
anonKey = (await prompt("Supabase anon key: ")).trim();
|
|
225
|
-
}
|
|
226
|
-
return { projectUrl, anonKey };
|
|
227
|
-
}
|
|
228
179
|
function formatTimestamp(date) {
|
|
229
180
|
const pad = (n) => String(n).padStart(2, "0");
|
|
230
181
|
return (String(date.getUTCFullYear()) +
|
|
@@ -268,3 +219,20 @@ function resolveContainedDir(cwd, dirRel) {
|
|
|
268
219
|
}
|
|
269
220
|
return dir;
|
|
270
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* The one statement to run once per Supabase project after `db push`: it
|
|
224
|
+
* stores the project's own URL and anon key in Vault, where the pg_cron jobs
|
|
225
|
+
* and pg_net webhooks read them (core/000_settings.sql). The migration files
|
|
226
|
+
* themselves carry no project-specific value, so the same files deploy to a
|
|
227
|
+
* sandbox and a production project.
|
|
228
|
+
*/
|
|
229
|
+
function settingsSeedSql(values = {}) {
|
|
230
|
+
const url = values.projectUrl?.trim() || "https://<ref>.supabase.co";
|
|
231
|
+
const key = values.anonKey?.trim() || "<anon key>";
|
|
232
|
+
return `select public.community_settings_set('${url.replace(/'/g, "''")}', '${key.replace(/'/g, "''")}');`;
|
|
233
|
+
}
|
|
234
|
+
function printSettingsSeedStep(log, values, step) {
|
|
235
|
+
log(` ${step}. Seed the project settings (once per Supabase project, after db push):`);
|
|
236
|
+
log(` supabase db query --linked "${settingsSeedSql(values)}"`);
|
|
237
|
+
log(" (or run that statement in the SQL editor — the migrations carry no project URL/key)");
|
|
238
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
-- community-sdk core/000_settings.sql (schema v1)
|
|
2
|
+
-- Community SDK — 00 project settings: where the pg_cron jobs and pg_net
|
|
3
|
+
-- webhooks learn this project's own URL and anon key.
|
|
4
|
+
--
|
|
5
|
+
-- They live in Supabase Vault, seeded ONCE PER PROJECT after `db push`:
|
|
6
|
+
--
|
|
7
|
+
-- select public.community_settings_set('https://<ref>.supabase.co', '<anon key>');
|
|
8
|
+
--
|
|
9
|
+
-- so the very same migration files deploy to a sandbox and a production
|
|
10
|
+
-- project. The anon key is public by design (it ships inside the app binary):
|
|
11
|
+
-- the Edge Functions do privileged work through their own service-role env,
|
|
12
|
+
-- the JWT only passes verify_jwt.
|
|
13
|
+
--
|
|
14
|
+
-- Every helper here is security definer and private (postgres + service_role):
|
|
15
|
+
-- the webhook trigger functions are security definer too, cron runs as
|
|
16
|
+
-- postgres, and nothing is exposed over PostgREST.
|
|
17
|
+
|
|
18
|
+
create extension if not exists supabase_vault;
|
|
19
|
+
create extension if not exists pg_cron;
|
|
20
|
+
create extension if not exists pg_net;
|
|
21
|
+
|
|
22
|
+
-- Upsert both settings. Idempotent: re-run it to rotate the key or move the
|
|
23
|
+
-- project.
|
|
24
|
+
create or replace function public.community_settings_set(p_project_url text, p_anon_key text)
|
|
25
|
+
returns void
|
|
26
|
+
language plpgsql
|
|
27
|
+
security definer set search_path = ''
|
|
28
|
+
as $$
|
|
29
|
+
declare
|
|
30
|
+
v_id uuid;
|
|
31
|
+
v_name text;
|
|
32
|
+
v_value text;
|
|
33
|
+
begin
|
|
34
|
+
for v_name, v_value in
|
|
35
|
+
values ('community_project_url', rtrim(p_project_url, '/')), ('community_anon_key', p_anon_key)
|
|
36
|
+
loop
|
|
37
|
+
select id into v_id from vault.secrets where name = v_name;
|
|
38
|
+
if v_id is null then
|
|
39
|
+
perform vault.create_secret(v_value, v_name);
|
|
40
|
+
else
|
|
41
|
+
perform vault.update_secret(v_id, v_value);
|
|
42
|
+
end if;
|
|
43
|
+
end loop;
|
|
44
|
+
end;
|
|
45
|
+
$$;
|
|
46
|
+
|
|
47
|
+
create or replace function public.community_setting(p_name text)
|
|
48
|
+
returns text
|
|
49
|
+
language plpgsql
|
|
50
|
+
stable
|
|
51
|
+
security definer set search_path = ''
|
|
52
|
+
as $$
|
|
53
|
+
declare
|
|
54
|
+
v_value text;
|
|
55
|
+
begin
|
|
56
|
+
select decrypted_secret into v_value from vault.decrypted_secrets where name = p_name;
|
|
57
|
+
if v_value is null then
|
|
58
|
+
raise exception 'community-sdk: project setting % is not seeded. Run once per project: select public.community_settings_set(''https://<ref>.supabase.co'', ''<anon key>'');', p_name;
|
|
59
|
+
end if;
|
|
60
|
+
return v_value;
|
|
61
|
+
end;
|
|
62
|
+
$$;
|
|
63
|
+
|
|
64
|
+
create or replace function public.community_project_url()
|
|
65
|
+
returns text language sql stable security definer set search_path = ''
|
|
66
|
+
as $$ select public.community_setting('community_project_url') $$;
|
|
67
|
+
|
|
68
|
+
create or replace function public.community_anon_key()
|
|
69
|
+
returns text language sql stable security definer set search_path = ''
|
|
70
|
+
as $$ select public.community_setting('community_anon_key') $$;
|
|
71
|
+
|
|
72
|
+
-- Call one of this project's own Edge Functions through pg_net. A failure
|
|
73
|
+
-- (settings not seeded, pg_net unavailable) is logged as a warning and never
|
|
74
|
+
-- aborts the insert/update that triggered it: the content stays `pending`
|
|
75
|
+
-- (or untranslated) and the daily sweep catches up.
|
|
76
|
+
create or replace function public.community_call_function(p_function text, p_body jsonb default null)
|
|
77
|
+
returns void
|
|
78
|
+
language plpgsql
|
|
79
|
+
security definer set search_path = ''
|
|
80
|
+
as $$
|
|
81
|
+
begin
|
|
82
|
+
perform net.http_post(
|
|
83
|
+
url := public.community_project_url() || '/functions/v1/' || p_function,
|
|
84
|
+
headers := jsonb_build_object(
|
|
85
|
+
'Content-Type', 'application/json',
|
|
86
|
+
'Authorization', 'Bearer ' || public.community_anon_key()
|
|
87
|
+
),
|
|
88
|
+
body := coalesce(p_body, '{}'::jsonb)
|
|
89
|
+
);
|
|
90
|
+
exception when others then
|
|
91
|
+
raise warning 'community-sdk: could not call Edge Function %: %', p_function, sqlerrm;
|
|
92
|
+
end;
|
|
93
|
+
$$;
|
|
94
|
+
|
|
95
|
+
-- (Re)schedule a cron job by name: replaces an existing job of the same name,
|
|
96
|
+
-- so a re-run (or an upgrade from a release that baked the URL into the job
|
|
97
|
+
-- command) always ends with exactly one job carrying the current command.
|
|
98
|
+
create or replace function public.community_schedule(p_name text, p_schedule text, p_command text)
|
|
99
|
+
returns void
|
|
100
|
+
language plpgsql
|
|
101
|
+
security definer set search_path = ''
|
|
102
|
+
as $$
|
|
103
|
+
declare
|
|
104
|
+
v_jobid bigint;
|
|
105
|
+
begin
|
|
106
|
+
for v_jobid in select jobid from cron.job where jobname = p_name loop
|
|
107
|
+
perform cron.unschedule(v_jobid);
|
|
108
|
+
end loop;
|
|
109
|
+
perform cron.schedule(p_name, p_schedule, p_command);
|
|
110
|
+
end;
|
|
111
|
+
$$;
|
|
112
|
+
|
|
113
|
+
revoke execute on function
|
|
114
|
+
public.community_settings_set(text, text),
|
|
115
|
+
public.community_setting(text),
|
|
116
|
+
public.community_project_url(),
|
|
117
|
+
public.community_anon_key(),
|
|
118
|
+
public.community_call_function(text, jsonb),
|
|
119
|
+
public.community_schedule(text, text, text)
|
|
120
|
+
from public, anon, authenticated;
|
|
121
|
+
grant execute on function
|
|
122
|
+
public.community_settings_set(text, text),
|
|
123
|
+
public.community_setting(text),
|
|
124
|
+
public.community_project_url(),
|
|
125
|
+
public.community_anon_key(),
|
|
126
|
+
public.community_call_function(text, jsonb),
|
|
127
|
+
public.community_schedule(text, text, text)
|
|
128
|
+
to service_role;
|
|
@@ -1,68 +1,7 @@
|
|
|
1
1
|
-- community-sdk core/003_moderation.sql (schema v1)
|
|
2
|
-
-- Community SDK — 03 moderation plumbing:
|
|
3
|
-
-- webhook
|
|
4
|
-
--
|
|
5
|
-
--
|
|
6
|
-
-- app's project URL (https://<ref>.supabase.co) and __SUPABASE_ANON_KEY__ with
|
|
7
|
-
-- its anon key. The anon key is public by design (shipped inside the app
|
|
8
|
-
-- binary): the Edge Functions do privileged work through their own
|
|
9
|
-
-- service-role env, the JWT only passes verify_jwt.
|
|
10
|
-
--
|
|
11
|
-
-- The DO block below fails the migration loudly if the placeholders were not
|
|
12
|
-
-- replaced.
|
|
13
|
-
do $$ begin
|
|
14
|
-
if '__SUPABASE_PROJECT_URL__' like '\_\_SUPABASE%' escape '\' then
|
|
15
|
-
raise exception 'community-sdk: placeholders not substituted. Run: npx @rocapine/community init';
|
|
16
|
-
end if;
|
|
17
|
-
end $$;
|
|
18
|
-
|
|
2
|
+
-- Community SDK — 03 moderation plumbing: the extensions the report webhook
|
|
3
|
+
-- and the daily moderation cron need. The webhook function, its trigger and
|
|
4
|
+
-- the cron job themselves live in core/008_webhooks.sql (they read the
|
|
5
|
+
-- project URL / anon key from the settings seeded via core/000_settings.sql).
|
|
19
6
|
create extension if not exists pg_cron;
|
|
20
7
|
create extension if not exists pg_net;
|
|
21
|
-
|
|
22
|
-
-- Real-time: forward each new report to the report-to-slack Edge Function.
|
|
23
|
-
create or replace function public.notify_report_webhook()
|
|
24
|
-
returns trigger
|
|
25
|
-
language plpgsql
|
|
26
|
-
security definer set search_path = public
|
|
27
|
-
as $$
|
|
28
|
-
begin
|
|
29
|
-
perform net.http_post(
|
|
30
|
-
url := '__SUPABASE_PROJECT_URL__/functions/v1/report-to-slack',
|
|
31
|
-
headers := jsonb_build_object(
|
|
32
|
-
'Content-Type', 'application/json',
|
|
33
|
-
'Authorization', 'Bearer __SUPABASE_ANON_KEY__'
|
|
34
|
-
),
|
|
35
|
-
body := jsonb_build_object('record', to_jsonb(new))
|
|
36
|
-
);
|
|
37
|
-
return new;
|
|
38
|
-
end;
|
|
39
|
-
$$;
|
|
40
|
-
|
|
41
|
-
drop trigger if exists on_report_created on public.reports;
|
|
42
|
-
create trigger on_report_created
|
|
43
|
-
after insert on public.reports
|
|
44
|
-
for each row execute function public.notify_report_webhook();
|
|
45
|
-
|
|
46
|
-
-- Daily 08:00 UTC moderation batch. Wrapped defensively: cron.schedule
|
|
47
|
-
-- upserts by job name, but if a prior manual run already created the job
|
|
48
|
-
-- and this raises anyway, do not fail the whole migration on a rerun.
|
|
49
|
-
do $$
|
|
50
|
-
begin
|
|
51
|
-
perform cron.schedule(
|
|
52
|
-
'daily-moderation',
|
|
53
|
-
'0 8 * * *',
|
|
54
|
-
$cron$
|
|
55
|
-
select net.http_post(
|
|
56
|
-
url := '__SUPABASE_PROJECT_URL__/functions/v1/daily-moderation',
|
|
57
|
-
headers := jsonb_build_object('Authorization', 'Bearer __SUPABASE_ANON_KEY__')
|
|
58
|
-
);
|
|
59
|
-
$cron$
|
|
60
|
-
);
|
|
61
|
-
exception when others then
|
|
62
|
-
if sqlerrm like '%already exists%' then
|
|
63
|
-
raise notice 'cron job daily-moderation already scheduled, skipping';
|
|
64
|
-
else
|
|
65
|
-
raise;
|
|
66
|
-
end if;
|
|
67
|
-
end;
|
|
68
|
-
$$;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- community-sdk core/008_webhooks.sql (schema v1)
|
|
2
|
+
-- Community SDK — 08 report webhook + daily moderation cron, built on the
|
|
3
|
+
-- project settings helpers (core/000_settings.sql). On an existing install
|
|
4
|
+
-- this replaces the function and job that carried the project URL baked in.
|
|
5
|
+
|
|
6
|
+
-- Real-time: forward each new report to the report-to-slack Edge Function.
|
|
7
|
+
create or replace function public.notify_report_webhook()
|
|
8
|
+
returns trigger
|
|
9
|
+
language plpgsql
|
|
10
|
+
security definer set search_path = public
|
|
11
|
+
as $$
|
|
12
|
+
begin
|
|
13
|
+
perform public.community_call_function('report-to-slack', jsonb_build_object('record', to_jsonb(new)));
|
|
14
|
+
return new;
|
|
15
|
+
end;
|
|
16
|
+
$$;
|
|
17
|
+
|
|
18
|
+
create or replace trigger on_report_created
|
|
19
|
+
after insert on public.reports
|
|
20
|
+
for each row execute function public.notify_report_webhook();
|
|
21
|
+
|
|
22
|
+
-- Daily 08:00 UTC moderation batch.
|
|
23
|
+
select public.community_schedule(
|
|
24
|
+
'daily-moderation',
|
|
25
|
+
'0 8 * * *',
|
|
26
|
+
$cron$ select public.community_call_function('daily-moderation'); $cron$
|
|
27
|
+
);
|
|
@@ -1,89 +1,9 @@
|
|
|
1
1
|
-- community-sdk push/002_triggers.sql (schema v1)
|
|
2
|
-
-- Community SDK — push module (OPTIONAL):
|
|
3
|
-
--
|
|
4
|
-
--
|
|
2
|
+
-- Community SDK — push module (OPTIONAL): the extensions the push webhooks
|
|
3
|
+
-- need. The webhook functions, triggers and the hourly digest cron live in
|
|
4
|
+
-- push/003_webhooks.sql (they read the project URL / anon key from the
|
|
5
|
+
-- settings seeded via core/000_settings.sql). References core module tables
|
|
5
6
|
-- (public.comments, public.likes) — the core module must be installed first,
|
|
6
7
|
-- and push/001_push.sql (push_tokens) before this file.
|
|
7
|
-
--
|
|
8
|
-
-- ⚠️ PLACEHOLDERS: before pushing, replace __SUPABASE_PROJECT_URL__ with the
|
|
9
|
-
-- app's project URL (https://<ref>.supabase.co) and __SUPABASE_ANON_KEY__ with
|
|
10
|
-
-- its anon key. The anon key is public by design (shipped inside the app
|
|
11
|
-
-- binary): the Edge Functions do privileged work through their own
|
|
12
|
-
-- service-role env, the JWT only passes verify_jwt.
|
|
13
|
-
--
|
|
14
|
-
-- The DO block below fails the migration loudly if the placeholders were not
|
|
15
|
-
-- replaced.
|
|
16
|
-
do $$ begin
|
|
17
|
-
if '__SUPABASE_PROJECT_URL__' like '\_\_SUPABASE%' escape '\' then
|
|
18
|
-
raise exception 'community-sdk: placeholders not substituted. Run: npx @rocapine/community init';
|
|
19
|
-
end if;
|
|
20
|
-
end $$;
|
|
21
|
-
|
|
22
8
|
create extension if not exists pg_cron;
|
|
23
9
|
create extension if not exists pg_net;
|
|
24
|
-
|
|
25
|
-
-- ============ WEBHOOK FUNCTIONS ============
|
|
26
|
-
create or replace function public.notify_comment_webhook()
|
|
27
|
-
returns trigger language plpgsql security definer set search_path = public as $$
|
|
28
|
-
begin
|
|
29
|
-
perform net.http_post(
|
|
30
|
-
url := '__SUPABASE_PROJECT_URL__/functions/v1/notify-comment',
|
|
31
|
-
headers := jsonb_build_object('Content-Type','application/json','Authorization','Bearer __SUPABASE_ANON_KEY__'),
|
|
32
|
-
body := jsonb_build_object('record', to_jsonb(new))
|
|
33
|
-
);
|
|
34
|
-
return new;
|
|
35
|
-
end; $$;
|
|
36
|
-
|
|
37
|
-
create or replace function public.notify_like_webhook()
|
|
38
|
-
returns trigger language plpgsql security definer set search_path = public as $$
|
|
39
|
-
begin
|
|
40
|
-
perform net.http_post(
|
|
41
|
-
url := '__SUPABASE_PROJECT_URL__/functions/v1/notify-like',
|
|
42
|
-
headers := jsonb_build_object('Content-Type','application/json','Authorization','Bearer __SUPABASE_ANON_KEY__'),
|
|
43
|
-
body := jsonb_build_object('record', to_jsonb(new))
|
|
44
|
-
);
|
|
45
|
-
return new;
|
|
46
|
-
end; $$;
|
|
47
|
-
|
|
48
|
-
-- ============ TRIGGERS ============
|
|
49
|
-
-- Notify the post author only once a comment is actually published, never on
|
|
50
|
-
-- the pending insert (a rejected comment must never send a push).
|
|
51
|
-
create trigger on_comment_published
|
|
52
|
-
after update on public.comments
|
|
53
|
-
for each row
|
|
54
|
-
when (old.status = 'pending' and new.status = 'visible')
|
|
55
|
-
execute function public.notify_comment_webhook();
|
|
56
|
-
|
|
57
|
-
-- Dashboard comments are inserted directly as 'visible' (service role) and
|
|
58
|
-
-- must notify too. App comments are always inserted 'pending' (RLS-enforced),
|
|
59
|
-
-- so nothing double-notifies.
|
|
60
|
-
create trigger on_comment_created_visible
|
|
61
|
-
after insert on public.comments
|
|
62
|
-
for each row
|
|
63
|
-
when (new.status = 'visible')
|
|
64
|
-
execute function public.notify_comment_webhook();
|
|
65
|
-
|
|
66
|
-
create trigger on_like_created
|
|
67
|
-
after insert on public.likes
|
|
68
|
-
for each row execute function public.notify_like_webhook();
|
|
69
|
-
|
|
70
|
-
-- Hourly digest: flush the tail of any coalesced like bursts.
|
|
71
|
-
do $$
|
|
72
|
-
begin
|
|
73
|
-
perform cron.schedule(
|
|
74
|
-
'community-like-digest', '0 * * * *',
|
|
75
|
-
$cron$
|
|
76
|
-
select net.http_post(
|
|
77
|
-
url := '__SUPABASE_PROJECT_URL__/functions/v1/notify-like',
|
|
78
|
-
headers := jsonb_build_object('Content-Type','application/json','Authorization','Bearer __SUPABASE_ANON_KEY__')
|
|
79
|
-
);
|
|
80
|
-
$cron$
|
|
81
|
-
);
|
|
82
|
-
exception when others then
|
|
83
|
-
if sqlerrm like '%already exists%' then
|
|
84
|
-
raise notice 'cron job community-like-digest already scheduled, skipping';
|
|
85
|
-
else
|
|
86
|
-
raise;
|
|
87
|
-
end if;
|
|
88
|
-
end;
|
|
89
|
-
$$;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- community-sdk push/003_webhooks.sql (schema v1)
|
|
2
|
+
-- Community SDK — push module: webhook functions, triggers and the hourly
|
|
3
|
+
-- digest cron, built on the project settings helpers (core/000_settings.sql).
|
|
4
|
+
-- Requires the notify-comment / notify-like Edge Functions. On an existing
|
|
5
|
+
-- install this replaces the functions and job that carried the project URL
|
|
6
|
+
-- baked in.
|
|
7
|
+
|
|
8
|
+
-- ============ WEBHOOK FUNCTIONS ============
|
|
9
|
+
create or replace function public.notify_comment_webhook()
|
|
10
|
+
returns trigger language plpgsql security definer set search_path = public as $$
|
|
11
|
+
begin
|
|
12
|
+
perform public.community_call_function('notify-comment', jsonb_build_object('record', to_jsonb(new)));
|
|
13
|
+
return new;
|
|
14
|
+
end; $$;
|
|
15
|
+
|
|
16
|
+
create or replace function public.notify_like_webhook()
|
|
17
|
+
returns trigger language plpgsql security definer set search_path = public as $$
|
|
18
|
+
begin
|
|
19
|
+
perform public.community_call_function('notify-like', jsonb_build_object('record', to_jsonb(new)));
|
|
20
|
+
return new;
|
|
21
|
+
end; $$;
|
|
22
|
+
|
|
23
|
+
-- ============ TRIGGERS ============
|
|
24
|
+
-- Notify the post author only once a comment is actually published, never on
|
|
25
|
+
-- the pending insert (a rejected comment must never send a push).
|
|
26
|
+
create or replace trigger on_comment_published
|
|
27
|
+
after update on public.comments
|
|
28
|
+
for each row
|
|
29
|
+
when (old.status = 'pending' and new.status = 'visible')
|
|
30
|
+
execute function public.notify_comment_webhook();
|
|
31
|
+
|
|
32
|
+
-- Dashboard comments are inserted directly as 'visible' (service role) and
|
|
33
|
+
-- must notify too. App comments are always inserted 'pending' (RLS-enforced),
|
|
34
|
+
-- so nothing double-notifies.
|
|
35
|
+
create or replace trigger on_comment_created_visible
|
|
36
|
+
after insert on public.comments
|
|
37
|
+
for each row
|
|
38
|
+
when (new.status = 'visible')
|
|
39
|
+
execute function public.notify_comment_webhook();
|
|
40
|
+
|
|
41
|
+
create or replace trigger on_like_created
|
|
42
|
+
after insert on public.likes
|
|
43
|
+
for each row execute function public.notify_like_webhook();
|
|
44
|
+
|
|
45
|
+
-- Hourly digest: flush the tail of any coalesced like bursts.
|
|
46
|
+
select public.community_schedule(
|
|
47
|
+
'community-like-digest',
|
|
48
|
+
'0 * * * *',
|
|
49
|
+
$cron$ select public.community_call_function('notify-like'); $cron$
|
|
50
|
+
);
|
|
@@ -13,20 +13,9 @@
|
|
|
13
13
|
-- notify-reaction webhook below still fires either way — it is the Edge
|
|
14
14
|
-- Function's job to look up push_tokens if/when that module is present).
|
|
15
15
|
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
-- binary): the Edge Functions do privileged work through their own
|
|
20
|
-
-- service-role env, the JWT only passes verify_jwt.
|
|
21
|
-
--
|
|
22
|
-
-- The DO block below fails the migration loudly if the placeholders were not
|
|
23
|
-
-- replaced.
|
|
24
|
-
do $$ begin
|
|
25
|
-
if '__SUPABASE_PROJECT_URL__' like '\_\_SUPABASE%' escape '\' then
|
|
26
|
-
raise exception 'community-sdk: placeholders not substituted. Run: npx @rocapine/community init';
|
|
27
|
-
end if;
|
|
28
|
-
end $$;
|
|
29
|
-
|
|
16
|
+
-- The reaction webhook, its trigger and the half-hourly digest cron live in
|
|
17
|
+
-- reaction/002_webhooks.sql (they read the project URL / anon key from the
|
|
18
|
+
-- settings seeded via core/000_settings.sql).
|
|
30
19
|
create extension if not exists pg_cron;
|
|
31
20
|
create extension if not exists pg_net;
|
|
32
21
|
|
|
@@ -97,43 +86,3 @@ begin
|
|
|
97
86
|
alter table public.push_tokens add column if not exists notify_reactions boolean not null default true;
|
|
98
87
|
end if;
|
|
99
88
|
end $$;
|
|
100
|
-
|
|
101
|
-
-- ============ WEBHOOK + DIGEST ============
|
|
102
|
-
-- Same pattern as push/002_triggers.sql's notify-like: an immediate webhook
|
|
103
|
-
-- on insert plus a coalescing digest cron for anything the webhook missed.
|
|
104
|
-
-- Requires the notify-reaction Edge Function.
|
|
105
|
-
create or replace function public.notify_reaction_webhook()
|
|
106
|
-
returns trigger language plpgsql security definer set search_path = public as $$
|
|
107
|
-
begin
|
|
108
|
-
perform net.http_post(
|
|
109
|
-
url := '__SUPABASE_PROJECT_URL__/functions/v1/notify-reaction',
|
|
110
|
-
headers := jsonb_build_object('Content-Type','application/json','Authorization','Bearer __SUPABASE_ANON_KEY__'),
|
|
111
|
-
body := jsonb_build_object('record', to_jsonb(new))
|
|
112
|
-
);
|
|
113
|
-
return new;
|
|
114
|
-
end; $$;
|
|
115
|
-
|
|
116
|
-
create trigger on_post_reaction_created
|
|
117
|
-
after insert on public.post_reactions
|
|
118
|
-
for each row execute function public.notify_reaction_webhook();
|
|
119
|
-
|
|
120
|
-
-- Half-hourly digest: flush the tail of any coalesced reaction bursts.
|
|
121
|
-
do $$
|
|
122
|
-
begin
|
|
123
|
-
perform cron.schedule(
|
|
124
|
-
'community-reaction-digest', '30 * * * *',
|
|
125
|
-
$cron$
|
|
126
|
-
select net.http_post(
|
|
127
|
-
url := '__SUPABASE_PROJECT_URL__/functions/v1/notify-reaction',
|
|
128
|
-
headers := jsonb_build_object('Content-Type','application/json','Authorization','Bearer __SUPABASE_ANON_KEY__')
|
|
129
|
-
);
|
|
130
|
-
$cron$
|
|
131
|
-
);
|
|
132
|
-
exception when others then
|
|
133
|
-
if sqlerrm like '%already exists%' then
|
|
134
|
-
raise notice 'cron job community-reaction-digest already scheduled, skipping';
|
|
135
|
-
else
|
|
136
|
-
raise;
|
|
137
|
-
end if;
|
|
138
|
-
end;
|
|
139
|
-
$$;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- community-sdk reaction/002_webhooks.sql (schema v1)
|
|
2
|
+
-- Community SDK — reaction module: webhook + half-hourly digest, built on the
|
|
3
|
+
-- project settings helpers (core/000_settings.sql). Same pattern as
|
|
4
|
+
-- push/003_webhooks.sql's notify-like: an immediate webhook on insert plus a
|
|
5
|
+
-- coalescing digest cron for anything the webhook missed. Requires the
|
|
6
|
+
-- notify-reaction Edge Function. On an existing install this replaces the
|
|
7
|
+
-- function and job that carried the project URL baked in.
|
|
8
|
+
create or replace function public.notify_reaction_webhook()
|
|
9
|
+
returns trigger language plpgsql security definer set search_path = public as $$
|
|
10
|
+
begin
|
|
11
|
+
perform public.community_call_function('notify-reaction', jsonb_build_object('record', to_jsonb(new)));
|
|
12
|
+
return new;
|
|
13
|
+
end; $$;
|
|
14
|
+
|
|
15
|
+
create or replace trigger on_post_reaction_created
|
|
16
|
+
after insert on public.post_reactions
|
|
17
|
+
for each row execute function public.notify_reaction_webhook();
|
|
18
|
+
|
|
19
|
+
-- Half-hourly digest: flush the tail of any coalesced reaction bursts.
|
|
20
|
+
select public.community_schedule(
|
|
21
|
+
'community-reaction-digest',
|
|
22
|
+
'30 * * * *',
|
|
23
|
+
$cron$ select public.community_call_function('notify-reaction'); $cron$
|
|
24
|
+
);
|
|
@@ -9,20 +9,9 @@
|
|
|
9
9
|
-- likes). Additive: no core table changes. poll_option_translations is
|
|
10
10
|
-- guarded so this module installs on a backend without the polls module.
|
|
11
11
|
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
-- binary): the Edge Functions do privileged work through their own
|
|
16
|
-
-- service-role env, the JWT only passes verify_jwt.
|
|
17
|
-
--
|
|
18
|
-
-- The DO block below fails the migration loudly if the placeholders were not
|
|
19
|
-
-- replaced.
|
|
20
|
-
do $$ begin
|
|
21
|
-
if '__SUPABASE_PROJECT_URL__' like '\_\_SUPABASE%' escape '\' then
|
|
22
|
-
raise exception 'community-sdk: placeholders not substituted. Run: npx @rocapine/community init';
|
|
23
|
-
end if;
|
|
24
|
-
end $$;
|
|
25
|
-
|
|
12
|
+
-- The translate webhooks, their triggers and the daily sweep cron live in
|
|
13
|
+
-- translation/002_webhooks.sql (they read the project URL / anon key from the
|
|
14
|
+
-- settings seeded via core/000_settings.sql).
|
|
26
15
|
create table public.post_translations (
|
|
27
16
|
post_id uuid not null references public.posts(id) on delete cascade,
|
|
28
17
|
locale text not null,
|
|
@@ -120,71 +109,3 @@ language sql stable security definer set search_path = public as $$
|
|
|
120
109
|
$$;
|
|
121
110
|
revoke execute on function public.items_missing_translations(text, text[], int) from public, anon, authenticated;
|
|
122
111
|
grant execute on function public.items_missing_translations(text, text[], int) to service_role;
|
|
123
|
-
|
|
124
|
-
-- ============ TRIGGERS ============
|
|
125
|
-
create or replace function public.translate_post_webhook()
|
|
126
|
-
returns trigger language plpgsql security definer set search_path = public as $$
|
|
127
|
-
begin
|
|
128
|
-
perform net.http_post(
|
|
129
|
-
url := '__SUPABASE_PROJECT_URL__/functions/v1/translate-one',
|
|
130
|
-
headers := jsonb_build_object('Content-Type','application/json','Authorization','Bearer __SUPABASE_ANON_KEY__'),
|
|
131
|
-
body := jsonb_build_object('kind', 'post', 'id', new.id)
|
|
132
|
-
);
|
|
133
|
-
return new;
|
|
134
|
-
end; $$;
|
|
135
|
-
|
|
136
|
-
create or replace function public.translate_comment_webhook()
|
|
137
|
-
returns trigger language plpgsql security definer set search_path = public as $$
|
|
138
|
-
begin
|
|
139
|
-
perform net.http_post(
|
|
140
|
-
url := '__SUPABASE_PROJECT_URL__/functions/v1/translate-one',
|
|
141
|
-
headers := jsonb_build_object('Content-Type','application/json','Authorization','Bearer __SUPABASE_ANON_KEY__'),
|
|
142
|
-
body := jsonb_build_object('kind', 'comment', 'id', new.id)
|
|
143
|
-
);
|
|
144
|
-
return new;
|
|
145
|
-
end; $$;
|
|
146
|
-
|
|
147
|
-
drop trigger if exists on_post_published_translate on public.posts;
|
|
148
|
-
create trigger on_post_published_translate
|
|
149
|
-
after update of status on public.posts for each row
|
|
150
|
-
when (old.status = 'pending' and new.status = 'visible')
|
|
151
|
-
execute function public.translate_post_webhook();
|
|
152
|
-
drop trigger if exists on_post_created_visible_translate on public.posts;
|
|
153
|
-
create trigger on_post_created_visible_translate
|
|
154
|
-
after insert on public.posts for each row
|
|
155
|
-
when (new.status = 'visible')
|
|
156
|
-
execute function public.translate_post_webhook();
|
|
157
|
-
|
|
158
|
-
drop trigger if exists on_comment_published_translate on public.comments;
|
|
159
|
-
create trigger on_comment_published_translate
|
|
160
|
-
after update of status on public.comments for each row
|
|
161
|
-
when (old.status = 'pending' and new.status = 'visible')
|
|
162
|
-
execute function public.translate_comment_webhook();
|
|
163
|
-
drop trigger if exists on_comment_created_visible_translate on public.comments;
|
|
164
|
-
create trigger on_comment_created_visible_translate
|
|
165
|
-
after insert on public.comments for each row
|
|
166
|
-
when (new.status = 'visible')
|
|
167
|
-
execute function public.translate_comment_webhook();
|
|
168
|
-
|
|
169
|
-
-- ============ CRON ============
|
|
170
|
-
-- Daily sweep at 08:30 UTC (after the 08:00 moderation sweep): back-fills the
|
|
171
|
-
-- history on first installation, then catches anything translate-one missed.
|
|
172
|
-
do $$
|
|
173
|
-
begin
|
|
174
|
-
perform cron.schedule(
|
|
175
|
-
'community-translation-sweep',
|
|
176
|
-
'30 8 * * *',
|
|
177
|
-
$cron$
|
|
178
|
-
select net.http_post(
|
|
179
|
-
url := '__SUPABASE_PROJECT_URL__/functions/v1/daily-translation',
|
|
180
|
-
headers := jsonb_build_object('Authorization', 'Bearer __SUPABASE_ANON_KEY__')
|
|
181
|
-
);
|
|
182
|
-
$cron$
|
|
183
|
-
);
|
|
184
|
-
exception when others then
|
|
185
|
-
if sqlerrm like '%already exists%' then
|
|
186
|
-
raise notice 'cron job community-translation-sweep already scheduled, skipping';
|
|
187
|
-
else
|
|
188
|
-
raise;
|
|
189
|
-
end if;
|
|
190
|
-
end $$;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- community-sdk translation/002_webhooks.sql (schema v1)
|
|
2
|
+
-- Community SDK — translation module: translate webhooks, triggers and the
|
|
3
|
+
-- daily sweep cron, built on the project settings helpers
|
|
4
|
+
-- (core/000_settings.sql). Requires the translate-one / daily-translation
|
|
5
|
+
-- Edge Functions. On an existing install this replaces the functions and job
|
|
6
|
+
-- that carried the project URL baked in.
|
|
7
|
+
|
|
8
|
+
-- ============ TRIGGERS ============
|
|
9
|
+
create or replace function public.translate_post_webhook()
|
|
10
|
+
returns trigger language plpgsql security definer set search_path = public as $$
|
|
11
|
+
begin
|
|
12
|
+
perform public.community_call_function('translate-one', jsonb_build_object('kind', 'post', 'id', new.id));
|
|
13
|
+
return new;
|
|
14
|
+
end; $$;
|
|
15
|
+
|
|
16
|
+
create or replace function public.translate_comment_webhook()
|
|
17
|
+
returns trigger language plpgsql security definer set search_path = public as $$
|
|
18
|
+
begin
|
|
19
|
+
perform public.community_call_function('translate-one', jsonb_build_object('kind', 'comment', 'id', new.id));
|
|
20
|
+
return new;
|
|
21
|
+
end; $$;
|
|
22
|
+
|
|
23
|
+
create or replace trigger on_post_published_translate
|
|
24
|
+
after update of status on public.posts for each row
|
|
25
|
+
when (old.status = 'pending' and new.status = 'visible')
|
|
26
|
+
execute function public.translate_post_webhook();
|
|
27
|
+
create or replace trigger on_post_created_visible_translate
|
|
28
|
+
after insert on public.posts for each row
|
|
29
|
+
when (new.status = 'visible')
|
|
30
|
+
execute function public.translate_post_webhook();
|
|
31
|
+
|
|
32
|
+
create or replace trigger on_comment_published_translate
|
|
33
|
+
after update of status on public.comments for each row
|
|
34
|
+
when (old.status = 'pending' and new.status = 'visible')
|
|
35
|
+
execute function public.translate_comment_webhook();
|
|
36
|
+
create or replace trigger on_comment_created_visible_translate
|
|
37
|
+
after insert on public.comments for each row
|
|
38
|
+
when (new.status = 'visible')
|
|
39
|
+
execute function public.translate_comment_webhook();
|
|
40
|
+
|
|
41
|
+
-- ============ CRON ============
|
|
42
|
+
-- Daily sweep at 08:30 UTC (after the 08:00 moderation sweep): back-fills the
|
|
43
|
+
-- history on first installation, then catches anything translate-one missed.
|
|
44
|
+
select public.community_schedule(
|
|
45
|
+
'community-translation-sweep',
|
|
46
|
+
'30 8 * * *',
|
|
47
|
+
$cron$ select public.community_call_function('daily-translation'); $cron$
|
|
48
|
+
);
|