ugly-app 0.1.761 → 0.1.762
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/dist/cli/feedbackResolve.d.ts.map +1 -1
- package/dist/cli/feedbackResolve.js +15 -39
- package/dist/cli/feedbackResolve.js.map +1 -1
- package/dist/cli/telemetryD1.d.ts +20 -0
- package/dist/cli/telemetryD1.d.ts.map +1 -0
- package/dist/cli/telemetryD1.js +41 -0
- package/dist/cli/telemetryD1.js.map +1 -0
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/package.json +1 -1
- package/src/cli/feedbackResolve.test.ts +66 -0
- package/src/cli/feedbackResolve.ts +18 -54
- package/src/cli/telemetryD1.ts +68 -0
- package/src/cli/version.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feedbackResolve.d.ts","sourceRoot":"","sources":["../../src/cli/feedbackResolve.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"feedbackResolve.d.ts","sourceRoot":"","sources":["../../src/cli/feedbackResolve.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,GAAG,UAAU,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,GAAG,UAAU,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;CACpB;AAoBD,wBAAsB,eAAe,CACnC,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,mBAAmB,EAAE,EAC5B,MAAM,CAAC,EAAE,CACP,IAAI,EAAE,mBAAmB,EACzB,MAAM,EAAE;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,KAChD,IAAI,GACR,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAAC,CAe3E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,EAAE,CA8BpE;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CASzD"}
|
|
@@ -1,43 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
async function resolveOne(appApiUrl, token, item) {
|
|
16
|
-
const res = await fetch(`${appApiUrl}/request`, {
|
|
17
|
-
method: 'POST',
|
|
18
|
-
headers: {
|
|
19
|
-
'Authorization': `Bearer ${token}`,
|
|
20
|
-
'Content-Type': 'application/json',
|
|
21
|
-
},
|
|
22
|
-
body: JSON.stringify({
|
|
23
|
-
function: 'feedbackReportResolve',
|
|
24
|
-
input: {
|
|
25
|
-
feedbackReportId: item.id,
|
|
26
|
-
status: item.status,
|
|
27
|
-
resolution: item.resolution,
|
|
28
|
-
},
|
|
29
|
-
}),
|
|
30
|
-
});
|
|
31
|
-
if (!res.ok) {
|
|
32
|
-
const body = await res.text().catch(() => '');
|
|
33
|
-
throw new Error(`${res.status} ${body}`);
|
|
1
|
+
import { execTelemetryD1 } from './telemetryD1.js';
|
|
2
|
+
/**
|
|
3
|
+
* Update a single feedback report's status + resolution directly in the
|
|
4
|
+
* project's PROD Cloudflare D1 (`feedback_report` table) — the same store the
|
|
5
|
+
* `feedback` read CLI queries. The previous implementation POSTed
|
|
6
|
+
* `{function:'feedbackReportResolve'}` to `ugly.bot/request`, an endpoint that
|
|
7
|
+
* lived on the retired `app` monolith; the live ugly.bot is a billable-op proxy
|
|
8
|
+
* that 400s ("Missing op"), so resolves silently failed for every child app.
|
|
9
|
+
*/
|
|
10
|
+
async function resolveOne(item) {
|
|
11
|
+
const { meta } = await execTelemetryD1('UPDATE feedback_report SET status = ?, resolution = ? WHERE id = ?', [item.status, item.resolution, item.id]);
|
|
12
|
+
if (!meta || (meta.changes ?? 0) === 0) {
|
|
13
|
+
throw new Error(`no feedback_report row with id "${item.id}"`);
|
|
34
14
|
}
|
|
35
15
|
}
|
|
36
16
|
export async function resolveFeedback(opts) {
|
|
37
|
-
const appApiUrl = getAppApiUrl();
|
|
38
|
-
const auth = readAuth();
|
|
39
17
|
try {
|
|
40
|
-
await resolveOne(
|
|
18
|
+
await resolveOne({
|
|
41
19
|
id: opts.feedbackId,
|
|
42
20
|
status: opts.status,
|
|
43
21
|
resolution: opts.resolution,
|
|
@@ -55,13 +33,11 @@ export async function resolveFeedback(opts) {
|
|
|
55
33
|
* leave items at `status: new` for a duplicate-process next cycle).
|
|
56
34
|
*/
|
|
57
35
|
export async function resolveFeedbackBatch(items, onItem) {
|
|
58
|
-
const appApiUrl = getAppApiUrl();
|
|
59
|
-
const auth = readAuth();
|
|
60
36
|
const failures = [];
|
|
61
37
|
let successes = 0;
|
|
62
38
|
for (const item of items) {
|
|
63
39
|
try {
|
|
64
|
-
await resolveOne(
|
|
40
|
+
await resolveOne(item);
|
|
65
41
|
successes++;
|
|
66
42
|
onItem?.(item, { ok: true });
|
|
67
43
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feedbackResolve.js","sourceRoot":"","sources":["../../src/cli/feedbackResolve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"feedbackResolve.js","sourceRoot":"","sources":["../../src/cli/feedbackResolve.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ProdNeonLookupOptions } from './prodDb.js';
|
|
2
|
+
export interface D1ExecResult {
|
|
3
|
+
results: Record<string, unknown>[];
|
|
4
|
+
/** D1 result meta — `changes` is the number of rows an UPDATE/DELETE modified. */
|
|
5
|
+
meta: {
|
|
6
|
+
changes?: number;
|
|
7
|
+
rows_written?: number;
|
|
8
|
+
rows_read?: number;
|
|
9
|
+
} | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Run a single SQL statement (read OR write) against the project's PRODUCTION
|
|
13
|
+
* Cloudflare D1 telemetry DB (errorLog / feedbackReport / …) via the HTTP API.
|
|
14
|
+
*
|
|
15
|
+
* This is the write-capable twin of the `feedback` / `errors` read path — both
|
|
16
|
+
* hit the same D1 database with the same publish-state-resolved token, so the
|
|
17
|
+
* CLI never has to route feedback mutations through a server endpoint.
|
|
18
|
+
*/
|
|
19
|
+
export declare function execTelemetryD1(sql: string, params?: unknown[], opts?: ProdNeonLookupOptions): Promise<D1ExecResult>;
|
|
20
|
+
//# sourceMappingURL=telemetryD1.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetryD1.d.ts","sourceRoot":"","sources":["../../src/cli/telemetryD1.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,qBAAqB,EAE3B,MAAM,aAAa,CAAC;AAIrB,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACnC,kFAAkF;IAClF,IAAI,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC9E;AAeD;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,OAAO,EAAO,EACtB,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,YAAY,CAAC,CA2BvB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getSecret } from './publish/secret-storage.js';
|
|
2
|
+
import { resolveProdTelemetryD1, } from './prodDb.js';
|
|
3
|
+
const CLOUDFLARE_API_BASE = 'https://api.cloudflare.com/client/v4';
|
|
4
|
+
/** Resolve the Cloudflare API token for a project's D1 (same lookup the read path uses). */
|
|
5
|
+
async function resolveD1Token(d1) {
|
|
6
|
+
const token = (await getSecret(`cloudflare:token:${d1.projectId}`)) ??
|
|
7
|
+
(await getSecret(`cloudflare:access-token:${d1.projectId}`));
|
|
8
|
+
if (!token) {
|
|
9
|
+
throw new Error(`No Cloudflare token for project "${d1.projectId}". Re-authenticate: \`ugly-app publish\`.`);
|
|
10
|
+
}
|
|
11
|
+
return token;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Run a single SQL statement (read OR write) against the project's PRODUCTION
|
|
15
|
+
* Cloudflare D1 telemetry DB (errorLog / feedbackReport / …) via the HTTP API.
|
|
16
|
+
*
|
|
17
|
+
* This is the write-capable twin of the `feedback` / `errors` read path — both
|
|
18
|
+
* hit the same D1 database with the same publish-state-resolved token, so the
|
|
19
|
+
* CLI never has to route feedback mutations through a server endpoint.
|
|
20
|
+
*/
|
|
21
|
+
export async function execTelemetryD1(sql, params = [], opts = {}) {
|
|
22
|
+
const d1 = resolveProdTelemetryD1(opts);
|
|
23
|
+
const token = await resolveD1Token(d1);
|
|
24
|
+
const res = await globalThis.fetch(`${CLOUDFLARE_API_BASE}/accounts/${d1.accountId}/d1/database/${d1.databaseId}/query`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${token}`,
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
Accept: 'application/json',
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ sql, params }),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const body = await res.text().catch(() => '');
|
|
35
|
+
throw new Error(`D1 query failed (${res.status}): ${body.slice(0, 200)}`);
|
|
36
|
+
}
|
|
37
|
+
const payload = (await res.json());
|
|
38
|
+
const first = payload.result?.[0];
|
|
39
|
+
return { results: first?.results ?? [], meta: first?.meta ?? null };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=telemetryD1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetryD1.js","sourceRoot":"","sources":["../../src/cli/telemetryD1.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxD,OAAO,EACL,sBAAsB,GAGvB,MAAM,aAAa,CAAC;AAErB,MAAM,mBAAmB,GAAG,sCAAsC,CAAC;AAQnE,4FAA4F;AAC5F,KAAK,UAAU,cAAc,CAAC,EAAmB;IAC/C,MAAM,KAAK,GACT,CAAC,MAAM,SAAS,CAAC,oBAAoB,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC;QACrD,CAAC,MAAM,SAAS,CAAC,2BAA2B,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,oCAAoC,EAAE,CAAC,SAAS,2CAA2C,CAC5F,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAW,EACX,SAAoB,EAAE,EACtB,OAA8B,EAAE;IAEhC,MAAM,EAAE,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,EAAE,CAAC,CAAC;IACvC,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,KAAK,CAChC,GAAG,mBAAmB,aAAa,EAAE,CAAC,SAAS,gBAAgB,EAAE,CAAC,UAAU,QAAQ,EACpF;QACE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,KAAK,EAAE;YAChC,cAAc,EAAE,kBAAkB;YAClC,MAAM,EAAE,kBAAkB;SAC3B;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;KACtC,CACF,CAAC;IACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,oBAAoB,GAAG,CAAC,MAAM,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAKhC,CAAC;IACF,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IAClC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC;AACtE,CAAC"}
|
package/dist/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.762";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ugly-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.762",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"comment:files": "Allowlist what ships to npm. dist = runtime; src = sourcemap targets (dist/*.js.map reference ../../src/); templates = CLI scaffold. Everything else at repo root (.pgdata local Postgres, coverage, assets/icons sources, test/, test-results/) is excluded by omission. The !negations strip the scaffold's installed deps + cruft (templates/node_modules is 200MB+ and must never ship). package.json/README/LICENSE ship automatically.",
|
|
6
6
|
"files": [
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// The resolve path writes to Cloudflare D1 via execTelemetryD1 — mock it so the
|
|
4
|
+
// unit test asserts the SQL/params contract without hitting the network.
|
|
5
|
+
vi.mock('./telemetryD1.js', () => ({ execTelemetryD1: vi.fn() }));
|
|
6
|
+
|
|
7
|
+
import { execTelemetryD1 } from './telemetryD1.js';
|
|
8
|
+
import {
|
|
9
|
+
parseBatchPayload,
|
|
10
|
+
resolveFeedback,
|
|
11
|
+
resolveFeedbackBatch,
|
|
12
|
+
} from './feedbackResolve.js';
|
|
13
|
+
|
|
14
|
+
const mockExec = vi.mocked(execTelemetryD1);
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockExec.mockReset();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('resolveFeedback', () => {
|
|
21
|
+
it('UPDATEs feedback_report with status + resolution keyed by id', async () => {
|
|
22
|
+
mockExec.mockResolvedValue({ results: [], meta: { changes: 1 } });
|
|
23
|
+
await resolveFeedback({ feedbackId: 'fb-1', status: 'resolved', resolution: 'done' });
|
|
24
|
+
expect(mockExec).toHaveBeenCalledTimes(1);
|
|
25
|
+
const [sql, params] = mockExec.mock.calls[0]!;
|
|
26
|
+
expect(sql).toMatch(/UPDATE\s+feedback_report\s+SET\s+status\s*=\s*\?,\s*resolution\s*=\s*\?\s+WHERE\s+id\s*=\s*\?/i);
|
|
27
|
+
expect(params).toEqual(['resolved', 'done', 'fb-1']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('throws when no row matched the id (changes === 0)', async () => {
|
|
31
|
+
mockExec.mockResolvedValue({ results: [], meta: { changes: 0 } });
|
|
32
|
+
await expect(
|
|
33
|
+
resolveFeedback({ feedbackId: 'missing', status: 'declined', resolution: 'n/a' }),
|
|
34
|
+
).rejects.toThrow(/no feedback_report row with id "missing"/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('throws when D1 returns no meta', async () => {
|
|
38
|
+
mockExec.mockResolvedValue({ results: [], meta: null });
|
|
39
|
+
await expect(
|
|
40
|
+
resolveFeedback({ feedbackId: 'x', status: 'resolved', resolution: 'y' }),
|
|
41
|
+
).rejects.toThrow(/Failed to resolve feedback/);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('resolveFeedbackBatch', () => {
|
|
46
|
+
it('processes every item and reports partial failures without throwing', async () => {
|
|
47
|
+
mockExec
|
|
48
|
+
.mockResolvedValueOnce({ results: [], meta: { changes: 1 } }) // ok
|
|
49
|
+
.mockResolvedValueOnce({ results: [], meta: { changes: 0 } }); // missing row
|
|
50
|
+
const { successes, failures } = await resolveFeedbackBatch([
|
|
51
|
+
{ id: 'a', status: 'resolved', resolution: 'r1' },
|
|
52
|
+
{ id: 'b', status: 'resolved', resolution: 'r2' },
|
|
53
|
+
]);
|
|
54
|
+
expect(successes).toBe(1);
|
|
55
|
+
expect(failures).toHaveLength(1);
|
|
56
|
+
expect(failures[0]!.id).toBe('b');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('parseBatchPayload', () => {
|
|
61
|
+
it('rejects an invalid status value', () => {
|
|
62
|
+
expect(() =>
|
|
63
|
+
parseBatchPayload(JSON.stringify([{ id: 'a', status: 'nope', resolution: 'x' }])),
|
|
64
|
+
).toThrow(/invalid status "nope"/);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { getUglyBotUrl } from '../shared/uglyBotUrl.js';
|
|
1
|
+
import { execTelemetryD1 } from './telemetryD1.js';
|
|
5
2
|
|
|
6
3
|
export interface FeedbackResolveOptions {
|
|
7
4
|
feedbackId: string;
|
|
@@ -15,60 +12,29 @@ export interface FeedbackResolveItem {
|
|
|
15
12
|
resolution: string;
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function getAppApiUrl(): string {
|
|
35
|
-
return getUglyBotUrl();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function resolveOne(
|
|
39
|
-
appApiUrl: string,
|
|
40
|
-
token: string,
|
|
41
|
-
item: FeedbackResolveItem,
|
|
42
|
-
): Promise<void> {
|
|
43
|
-
const res = await fetch(`${appApiUrl}/request`, {
|
|
44
|
-
method: 'POST',
|
|
45
|
-
headers: {
|
|
46
|
-
'Authorization': `Bearer ${token}`,
|
|
47
|
-
'Content-Type': 'application/json',
|
|
48
|
-
},
|
|
49
|
-
body: JSON.stringify({
|
|
50
|
-
function: 'feedbackReportResolve',
|
|
51
|
-
input: {
|
|
52
|
-
feedbackReportId: item.id,
|
|
53
|
-
status: item.status,
|
|
54
|
-
resolution: item.resolution,
|
|
55
|
-
},
|
|
56
|
-
}),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
if (!res.ok) {
|
|
60
|
-
const body = await res.text().catch(() => '');
|
|
61
|
-
throw new Error(`${res.status} ${body}`);
|
|
15
|
+
/**
|
|
16
|
+
* Update a single feedback report's status + resolution directly in the
|
|
17
|
+
* project's PROD Cloudflare D1 (`feedback_report` table) — the same store the
|
|
18
|
+
* `feedback` read CLI queries. The previous implementation POSTed
|
|
19
|
+
* `{function:'feedbackReportResolve'}` to `ugly.bot/request`, an endpoint that
|
|
20
|
+
* lived on the retired `app` monolith; the live ugly.bot is a billable-op proxy
|
|
21
|
+
* that 400s ("Missing op"), so resolves silently failed for every child app.
|
|
22
|
+
*/
|
|
23
|
+
async function resolveOne(item: FeedbackResolveItem): Promise<void> {
|
|
24
|
+
const { meta } = await execTelemetryD1(
|
|
25
|
+
'UPDATE feedback_report SET status = ?, resolution = ? WHERE id = ?',
|
|
26
|
+
[item.status, item.resolution, item.id],
|
|
27
|
+
);
|
|
28
|
+
if (!meta || (meta.changes ?? 0) === 0) {
|
|
29
|
+
throw new Error(`no feedback_report row with id "${item.id}"`);
|
|
62
30
|
}
|
|
63
31
|
}
|
|
64
32
|
|
|
65
33
|
export async function resolveFeedback(
|
|
66
34
|
opts: FeedbackResolveOptions,
|
|
67
35
|
): Promise<void> {
|
|
68
|
-
const appApiUrl = getAppApiUrl();
|
|
69
|
-
const auth = readAuth();
|
|
70
36
|
try {
|
|
71
|
-
await resolveOne(
|
|
37
|
+
await resolveOne({
|
|
72
38
|
id: opts.feedbackId,
|
|
73
39
|
status: opts.status,
|
|
74
40
|
resolution: opts.resolution,
|
|
@@ -92,13 +58,11 @@ export async function resolveFeedbackBatch(
|
|
|
92
58
|
result: { ok: true } | { ok: false; error: string },
|
|
93
59
|
) => void,
|
|
94
60
|
): Promise<{ successes: number; failures: { id: string; error: string }[] }> {
|
|
95
|
-
const appApiUrl = getAppApiUrl();
|
|
96
|
-
const auth = readAuth();
|
|
97
61
|
const failures: { id: string; error: string }[] = [];
|
|
98
62
|
let successes = 0;
|
|
99
63
|
for (const item of items) {
|
|
100
64
|
try {
|
|
101
|
-
await resolveOne(
|
|
65
|
+
await resolveOne(item);
|
|
102
66
|
successes++;
|
|
103
67
|
onItem?.(item, { ok: true });
|
|
104
68
|
} catch (e) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getSecret } from './publish/secret-storage.js';
|
|
2
|
+
import {
|
|
3
|
+
resolveProdTelemetryD1,
|
|
4
|
+
type ProdNeonLookupOptions,
|
|
5
|
+
type ProdTelemetryD1,
|
|
6
|
+
} from './prodDb.js';
|
|
7
|
+
|
|
8
|
+
const CLOUDFLARE_API_BASE = 'https://api.cloudflare.com/client/v4';
|
|
9
|
+
|
|
10
|
+
export interface D1ExecResult {
|
|
11
|
+
results: Record<string, unknown>[];
|
|
12
|
+
/** D1 result meta — `changes` is the number of rows an UPDATE/DELETE modified. */
|
|
13
|
+
meta: { changes?: number; rows_written?: number; rows_read?: number } | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Resolve the Cloudflare API token for a project's D1 (same lookup the read path uses). */
|
|
17
|
+
async function resolveD1Token(d1: ProdTelemetryD1): Promise<string> {
|
|
18
|
+
const token =
|
|
19
|
+
(await getSecret(`cloudflare:token:${d1.projectId}`)) ??
|
|
20
|
+
(await getSecret(`cloudflare:access-token:${d1.projectId}`));
|
|
21
|
+
if (!token) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`No Cloudflare token for project "${d1.projectId}". Re-authenticate: \`ugly-app publish\`.`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return token;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run a single SQL statement (read OR write) against the project's PRODUCTION
|
|
31
|
+
* Cloudflare D1 telemetry DB (errorLog / feedbackReport / …) via the HTTP API.
|
|
32
|
+
*
|
|
33
|
+
* This is the write-capable twin of the `feedback` / `errors` read path — both
|
|
34
|
+
* hit the same D1 database with the same publish-state-resolved token, so the
|
|
35
|
+
* CLI never has to route feedback mutations through a server endpoint.
|
|
36
|
+
*/
|
|
37
|
+
export async function execTelemetryD1(
|
|
38
|
+
sql: string,
|
|
39
|
+
params: unknown[] = [],
|
|
40
|
+
opts: ProdNeonLookupOptions = {},
|
|
41
|
+
): Promise<D1ExecResult> {
|
|
42
|
+
const d1 = resolveProdTelemetryD1(opts);
|
|
43
|
+
const token = await resolveD1Token(d1);
|
|
44
|
+
const res = await globalThis.fetch(
|
|
45
|
+
`${CLOUDFLARE_API_BASE}/accounts/${d1.accountId}/d1/database/${d1.databaseId}/query`,
|
|
46
|
+
{
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${token}`,
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
Accept: 'application/json',
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({ sql, params }),
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const body = await res.text().catch(() => '');
|
|
58
|
+
throw new Error(`D1 query failed (${res.status}): ${body.slice(0, 200)}`);
|
|
59
|
+
}
|
|
60
|
+
const payload = (await res.json()) as {
|
|
61
|
+
result?: {
|
|
62
|
+
results?: Record<string, unknown>[];
|
|
63
|
+
meta?: D1ExecResult['meta'];
|
|
64
|
+
}[];
|
|
65
|
+
};
|
|
66
|
+
const first = payload.result?.[0];
|
|
67
|
+
return { results: first?.results ?? [], meta: first?.meta ?? null };
|
|
68
|
+
}
|
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
2
|
+
export const CLI_VERSION = "0.1.762";
|