@cartanova/qgrid-cli 1.7.0 → 1.8.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 +67 -2
- package/bundle/dist/application/qgrid/oauth.js +5 -6
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +47 -32
- package/bundle/dist/application/qgrid/qgrid.frame.js +13 -25
- package/bundle/dist/application/qgrid/qgrid.types.js +10 -3
- package/bundle/dist/application/qgrid/token-subscriber.js +149 -0
- package/bundle/dist/application/qgrid/token-trigger-setup.js +69 -0
- package/bundle/dist/application/sonamu.generated.sso.js +1 -1
- package/bundle/dist/application/token/token.types.js +1 -1
- package/bundle/dist/i18n/sd.generated.js +2 -2
- package/bundle/dist/migrations/20260410171405_create__request_logs.js +1 -1
- package/bundle/dist/migrations/20260410171406_create__tokens.js +1 -1
- package/bundle/dist/migrations/20260411120624_alter_tokens_alter2.js +1 -1
- package/bundle/dist/migrations/20260415134649_alter_request_logs_alter1.js +1 -1
- package/bundle/dist/migrations/20260421223627_alter_request_logs_add1_alter1.js +1 -1
- package/bundle/dist/migrations/20260423180818_alter_request_logs_add1_alter1.js +1 -1
- package/bundle/dist/migrations/20260423182017_alter_request_logs_add2_drop1_alter1.js +1 -1
- package/bundle/dist/migrations/20260423183613_alter_request_logs_add1_alter1.js +1 -1
- package/bundle/dist/sonamu.config.js +41 -46
- package/bundle/dist/testing/setup-mocks.js +1 -1
- package/bundle/dist/utils/subset-loaders.js +1 -1
- package/bundle/src/application/qgrid/oauth.ts +5 -2
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +72 -32
- package/bundle/src/application/qgrid/qgrid.frame.ts +14 -28
- package/bundle/src/application/qgrid/qgrid.types.ts +9 -0
- package/bundle/src/application/qgrid/token-subscriber.ts +176 -0
- package/bundle/src/application/qgrid/token-trigger-setup.ts +80 -0
- package/bundle/src/application/request-log/request-log.entity.json +3 -1
- package/bundle/src/application/sonamu.generated.sso.ts +5 -5
- package/bundle/src/application/token/token.types.ts +1 -1
- package/bundle/src/i18n/sd.generated.ts +1 -1
- package/bundle/src/migrations/20260410171405_create__request_logs.ts +1 -1
- package/bundle/src/migrations/20260410171406_create__tokens.ts +1 -1
- package/bundle/src/migrations/20260411120624_alter_tokens_alter2.ts +1 -1
- package/bundle/src/migrations/20260415134649_alter_request_logs_alter1.ts +1 -1
- package/bundle/src/migrations/20260421223627_alter_request_logs_add1_alter1.ts +1 -1
- package/bundle/src/migrations/20260423180818_alter_request_logs_add1_alter1.ts +1 -1
- package/bundle/src/migrations/20260423182017_alter_request_logs_add2_drop1_alter1.ts +1 -1
- package/bundle/src/migrations/20260423183613_alter_request_logs_add1_alter1.ts +1 -1
- package/bundle/src/sonamu.config.ts +40 -50
- package/bundle/src/testing/setup-mocks.ts +1 -1
- package/bundle/src/typings/fastify.d.ts +1 -1
- package/bundle/src/typings/sonamu.d.ts +3 -2
- package/bundle/src/utils/subset-loaders.ts +1 -1
- package/bundle/web-dist/client/assets/{index-DUmwX7rR.js → index-DzEE3oyG.js} +3 -3
- package/bundle/web-dist/client/assets/{logs-ci_3vzTT.js → logs-Bn56eAAL.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-Ch5LFN0K.js → routes-BCEPwpzm.js} +1 -1
- package/bundle/web-dist/client/assets/{sd.generated-CIOZewtL.js → sd.generated-C9WnMJm4.js} +1 -1
- package/bundle/web-dist/client/assets/{services.generated-BrB-RELO.js → services.generated-CWN1-bYW.js} +1 -1
- package/bundle/web-dist/client/assets/{show-D3N15Krg.js → show-Ch7LEdBK.js} +1 -1
- package/bundle/web-dist/client/assets/tokens-DPvuQIs2.js +1 -0
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{copy-CKCGxwII.js → copy-SIUvcEcZ.js} +1 -1
- package/bundle/web-dist/server/assets/{lazyRouteComponent-BlvOfJIF.js → lazyRouteComponent-CcyhCAoL.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-CDacVwTQ.js → logs-D1ezT2IS.js} +3 -3
- package/bundle/web-dist/server/assets/{routes-BLm-zF86.js → routes-CtTFrsIz.js} +2 -2
- package/bundle/web-dist/server/assets/{sd.generated-CIg_jf8e.js → sd.generated-CkiEsOwK.js} +1 -1
- package/bundle/web-dist/server/assets/{services.generated-wCfUuY7Z.js → services.generated-D4asAlWb.js} +1 -1
- package/bundle/web-dist/server/assets/{shim-Dq_Izqnc.js → shim-BYpcQGN1.js} +1 -1
- package/bundle/web-dist/server/assets/{show-DjJyZunM.js → show-Bz0If-L7.js} +4 -4
- package/bundle/web-dist/server/assets/{tokens-rQIIgqMo.js → tokens-BBBfLC7N.js} +11 -4
- package/bundle/web-dist/server/entry-server.generated.js +7 -7
- package/package.json +1 -1
- package/bundle/web-dist/client/assets/tokens-BAlIaa6X.js +0 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/** tokens_changed LISTEN subscriber. */
|
|
2
|
+
import { getLogger } from "@logtape/logtape";
|
|
3
|
+
import { Client, type ClientConfig } from "pg";
|
|
4
|
+
|
|
5
|
+
import { TokenModel } from "../token/token.model";
|
|
6
|
+
import { type QgridDispatcherClass } from "./qgrid.dispatcher";
|
|
7
|
+
import { type SubscriberStatus } from "./qgrid.types";
|
|
8
|
+
|
|
9
|
+
const logger = getLogger(["qgrid", "subscriber"]);
|
|
10
|
+
|
|
11
|
+
const TOKENS_CHANGED = "tokens_changed";
|
|
12
|
+
const RECONCILE_INTERVAL_MS = 10 * 60 * 1000;
|
|
13
|
+
const CONNECTION_TIMEOUT_MS = 5_000;
|
|
14
|
+
const BACKOFF_BASE_MS = 500;
|
|
15
|
+
const BACKOFF_CAP_MS = 30_000;
|
|
16
|
+
|
|
17
|
+
type Payload = { op: "INSERT" | "UPDATE" | "DELETE"; id: number };
|
|
18
|
+
|
|
19
|
+
export class TokenSubscriber {
|
|
20
|
+
client: Client | null = null;
|
|
21
|
+
reconcileTimer: NodeJS.Timeout | null = null;
|
|
22
|
+
reconnectTimer: NodeJS.Timeout | null = null;
|
|
23
|
+
shutdownRequested = false;
|
|
24
|
+
attempt = 0;
|
|
25
|
+
connectedAt: Date | null = null;
|
|
26
|
+
lastReconcileAt: Date | null = null;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
public connConfig: ClientConfig,
|
|
30
|
+
public dispatcher: QgridDispatcherClass,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
async start(): Promise<boolean> {
|
|
34
|
+
this.shutdownRequested = false;
|
|
35
|
+
this.ensureReconcileTimer();
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await this.connectAndReconcile();
|
|
39
|
+
return true;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
logger.warn(`subscriber start failed: ${(e as Error).message}, scheduling retry`);
|
|
42
|
+
this.retryLater();
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async stop(): Promise<void> {
|
|
48
|
+
this.shutdownRequested = true;
|
|
49
|
+
if (this.reconcileTimer) {
|
|
50
|
+
clearInterval(this.reconcileTimer);
|
|
51
|
+
this.reconcileTimer = null;
|
|
52
|
+
}
|
|
53
|
+
if (this.reconnectTimer) {
|
|
54
|
+
clearTimeout(this.reconnectTimer);
|
|
55
|
+
this.reconnectTimer = null;
|
|
56
|
+
}
|
|
57
|
+
await this.closeClient();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
status(): SubscriberStatus {
|
|
61
|
+
return {
|
|
62
|
+
connected: this.client !== null && this.reconnectTimer === null,
|
|
63
|
+
connectedAt: this.connectedAt,
|
|
64
|
+
lastReconcileAt: this.lastReconcileAt,
|
|
65
|
+
attempt: this.attempt,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ensureReconcileTimer(): void {
|
|
70
|
+
if (this.reconcileTimer) return;
|
|
71
|
+
|
|
72
|
+
// LISTEN/NOTIFY 는 끊긴 동안 유실될 수 있어 주기적으로 DB 기준으로 맞춘다.
|
|
73
|
+
this.reconcileTimer = setInterval(() => {
|
|
74
|
+
this.reconcile().catch((e) =>
|
|
75
|
+
logger.warn(`periodic reconcile failed: ${(e as Error).message}`),
|
|
76
|
+
);
|
|
77
|
+
}, RECONCILE_INTERVAL_MS);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async connectAndReconcile(): Promise<void> {
|
|
81
|
+
await this.connectAndListen();
|
|
82
|
+
await this.reconcile();
|
|
83
|
+
this.attempt = 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async connectAndListen(): Promise<void> {
|
|
87
|
+
const client = new Client({
|
|
88
|
+
...this.connConfig,
|
|
89
|
+
connectionTimeoutMillis: CONNECTION_TIMEOUT_MS,
|
|
90
|
+
keepAlive: true,
|
|
91
|
+
keepAliveInitialDelayMillis: 10_000,
|
|
92
|
+
application_name: "qgrid-listener",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
client.on("error", (e) => {
|
|
96
|
+
logger.warn(`subscriber error: ${e.message}`);
|
|
97
|
+
this.retryLater();
|
|
98
|
+
});
|
|
99
|
+
client.on("end", () => {
|
|
100
|
+
if (!this.shutdownRequested) this.retryLater();
|
|
101
|
+
});
|
|
102
|
+
client.on("notification", (msg) => {
|
|
103
|
+
if (msg.channel !== TOKENS_CHANGED || !msg.payload) return;
|
|
104
|
+
this.handleNotification(msg.payload).catch((e) =>
|
|
105
|
+
logger.warn(`handle NOTIFY failed: ${(e as Error).message}`),
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await client.connect();
|
|
111
|
+
await client.query("SET statement_timeout = 0");
|
|
112
|
+
await client.query("SET idle_in_transaction_session_timeout = 0");
|
|
113
|
+
await client.query(`LISTEN ${client.escapeIdentifier(TOKENS_CHANGED)}`);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
client.removeAllListeners();
|
|
116
|
+
await client.end().catch(() => {});
|
|
117
|
+
throw e;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.client = client;
|
|
121
|
+
this.connectedAt = new Date();
|
|
122
|
+
logger.info(`subscribed to ${TOKENS_CHANGED}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
retryLater(): void {
|
|
126
|
+
if (this.shutdownRequested) return;
|
|
127
|
+
if (this.reconnectTimer) return;
|
|
128
|
+
|
|
129
|
+
void this.closeClient();
|
|
130
|
+
this.attempt += 1;
|
|
131
|
+
const cap = Math.min(BACKOFF_BASE_MS * 2 ** this.attempt, BACKOFF_CAP_MS);
|
|
132
|
+
const delay = Math.floor(Math.random() * cap);
|
|
133
|
+
logger.info(`reconnecting in ${delay}ms (attempt ${this.attempt})`);
|
|
134
|
+
this.reconnectTimer = setTimeout(() => {
|
|
135
|
+
this.reconnectTimer = null;
|
|
136
|
+
this.connectAndReconcile().catch((e) => {
|
|
137
|
+
logger.warn(`subscriber reconnect failed: ${(e as Error).message}`);
|
|
138
|
+
this.retryLater();
|
|
139
|
+
});
|
|
140
|
+
}, delay);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async closeClient(): Promise<void> {
|
|
144
|
+
const client = this.client;
|
|
145
|
+
if (!client) return;
|
|
146
|
+
|
|
147
|
+
this.client = null;
|
|
148
|
+
client.removeAllListeners();
|
|
149
|
+
await client.end().catch(() => {});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async handleNotification(payloadJson: string): Promise<void> {
|
|
153
|
+
const payload = JSON.parse(payloadJson) as Payload;
|
|
154
|
+
if (payload.op === "DELETE") {
|
|
155
|
+
this.dispatcher.removeCache(payload.id);
|
|
156
|
+
logger.info(`NOTIFY ${payload.op} id=${payload.id} → removed from cache`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const row = await TokenModel.findOne("A", { id: payload.id });
|
|
160
|
+
if (!row || !row.active) {
|
|
161
|
+
this.dispatcher.removeCache(payload.id);
|
|
162
|
+
logger.info(
|
|
163
|
+
`NOTIFY ${payload.op} id=${payload.id} (${row?.name ?? "?"}) active=${row?.active ?? "missing"} → removed from cache`,
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.dispatcher.upsertCache(payload.id, row);
|
|
168
|
+
logger.info(`NOTIFY ${payload.op} id=${payload.id} (${row.name}) active=true → cache upserted`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async reconcile(): Promise<void> {
|
|
172
|
+
const rows = await TokenModel.findActive("A");
|
|
173
|
+
this.dispatcher.replaceCache(rows);
|
|
174
|
+
this.lastReconcileAt = new Date();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** One-time setup for public.tokens -> tokens_changed NOTIFY triggers. */
|
|
2
|
+
import { getLogger } from "@logtape/logtape";
|
|
3
|
+
import { Client, type ClientConfig } from "pg";
|
|
4
|
+
|
|
5
|
+
const logger = getLogger(["qgrid", "trigger-setup"]);
|
|
6
|
+
|
|
7
|
+
const CONNECTION_TIMEOUT_MS = 5_000;
|
|
8
|
+
const SETUP_LOCK_TIMEOUT_MS = 3_000;
|
|
9
|
+
const SETUP_STATEMENT_TIMEOUT_MS = 10_000;
|
|
10
|
+
|
|
11
|
+
const ADVISORY_LOCK_CLASS_ID = 717;
|
|
12
|
+
const ADVISORY_LOCK_OBJECT_ID = 44900;
|
|
13
|
+
|
|
14
|
+
const SETUP_SQL = `
|
|
15
|
+
CREATE OR REPLACE FUNCTION public.tokens_notify() RETURNS trigger
|
|
16
|
+
LANGUAGE plpgsql
|
|
17
|
+
SET search_path = pg_catalog, public
|
|
18
|
+
AS $func$
|
|
19
|
+
BEGIN
|
|
20
|
+
PERFORM pg_notify('tokens_changed', json_build_object(
|
|
21
|
+
'op', TG_OP,
|
|
22
|
+
'id', COALESCE(NEW.id, OLD.id)
|
|
23
|
+
)::text);
|
|
24
|
+
RETURN COALESCE(NEW, OLD);
|
|
25
|
+
END;
|
|
26
|
+
$func$;
|
|
27
|
+
|
|
28
|
+
DROP TRIGGER IF EXISTS tokens_changed_trigger ON public.tokens;
|
|
29
|
+
DROP TRIGGER IF EXISTS tokens_changed ON public.tokens;
|
|
30
|
+
|
|
31
|
+
CREATE OR REPLACE TRIGGER tokens_changed_ins_del
|
|
32
|
+
AFTER INSERT OR DELETE ON public.tokens
|
|
33
|
+
FOR EACH ROW EXECUTE FUNCTION public.tokens_notify();
|
|
34
|
+
|
|
35
|
+
CREATE OR REPLACE TRIGGER tokens_changed_upd
|
|
36
|
+
AFTER UPDATE ON public.tokens
|
|
37
|
+
FOR EACH ROW
|
|
38
|
+
WHEN (
|
|
39
|
+
OLD.active IS DISTINCT FROM NEW.active OR
|
|
40
|
+
OLD.token IS DISTINCT FROM NEW.token OR
|
|
41
|
+
OLD.refresh_token IS DISTINCT FROM NEW.refresh_token OR
|
|
42
|
+
OLD.expires_at IS DISTINCT FROM NEW.expires_at OR
|
|
43
|
+
OLD.name IS DISTINCT FROM NEW.name
|
|
44
|
+
)
|
|
45
|
+
EXECUTE FUNCTION public.tokens_notify();
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
export async function ensureTokensTrigger(connConfig: ClientConfig): Promise<void> {
|
|
49
|
+
const client = new Client({
|
|
50
|
+
...connConfig,
|
|
51
|
+
connectionTimeoutMillis: CONNECTION_TIMEOUT_MS,
|
|
52
|
+
application_name: "qgrid-trigger-setup",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
let lockAcquired = false;
|
|
56
|
+
try {
|
|
57
|
+
await client.connect();
|
|
58
|
+
await client.query("SET search_path TO public");
|
|
59
|
+
await client.query(`SET lock_timeout = '${SETUP_LOCK_TIMEOUT_MS}ms'`);
|
|
60
|
+
await client.query(`SET statement_timeout = '${SETUP_STATEMENT_TIMEOUT_MS}ms'`);
|
|
61
|
+
await client.query("SELECT pg_advisory_lock($1, $2)", [
|
|
62
|
+
ADVISORY_LOCK_CLASS_ID,
|
|
63
|
+
ADVISORY_LOCK_OBJECT_ID,
|
|
64
|
+
]);
|
|
65
|
+
lockAcquired = true;
|
|
66
|
+
|
|
67
|
+
await client.query(SETUP_SQL);
|
|
68
|
+
logger.info("trigger ensured (public.tokens_notify + tokens_changed_* triggers)");
|
|
69
|
+
} finally {
|
|
70
|
+
if (lockAcquired) {
|
|
71
|
+
await client
|
|
72
|
+
.query("SELECT pg_advisory_unlock($1, $2)", [
|
|
73
|
+
ADVISORY_LOCK_CLASS_ID,
|
|
74
|
+
ADVISORY_LOCK_OBJECT_ID,
|
|
75
|
+
])
|
|
76
|
+
.catch(() => {});
|
|
77
|
+
}
|
|
78
|
+
await client.end().catch(() => {});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -97,7 +97,9 @@
|
|
|
97
97
|
"duration_ms",
|
|
98
98
|
"cost_usd"
|
|
99
99
|
],
|
|
100
|
-
"cone": {
|
|
100
|
+
"cone": {
|
|
101
|
+
"note": "리스트 조회용 Preview 서브셋. TEXT 컬럼(user_prompt, system_prompt, response) 제외"
|
|
102
|
+
}
|
|
101
103
|
}
|
|
102
104
|
},
|
|
103
105
|
"enums": {
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* 직접 수정하지 마세요.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { PuriWrapper, DatabaseSchemaExtend, PuriLoaderQueries } from "sonamu";
|
|
6
|
+
import { type PuriWrapper, type DatabaseSchemaExtend, type PuriLoaderQueries } from "sonamu";
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
RequestLogSubsetKey,
|
|
10
|
-
TokenSubsetKey,
|
|
11
|
-
RequestLogBaseSchema,
|
|
12
|
-
TokenBaseSchema,
|
|
9
|
+
type RequestLogSubsetKey,
|
|
10
|
+
type TokenSubsetKey,
|
|
11
|
+
type RequestLogBaseSchema,
|
|
12
|
+
type TokenBaseSchema,
|
|
13
13
|
} from "./sonamu.generated";
|
|
14
14
|
|
|
15
15
|
// SubsetQuery: RequestLog
|
|
@@ -5,23 +5,27 @@ import { getPrettyFormatter } from "@logtape/pretty";
|
|
|
5
5
|
import dotenv from "dotenv";
|
|
6
6
|
import { CachePresets, defineConfig } from "sonamu";
|
|
7
7
|
import { drivers as cacheDrivers, store } from "sonamu/cache";
|
|
8
|
-
import { drivers } from "sonamu/storage";
|
|
9
8
|
|
|
10
9
|
dotenv.config({ path: path.join(import.meta.dirname, "../.env") });
|
|
11
10
|
|
|
12
11
|
const host = process.env.HOST ?? "localhost";
|
|
13
12
|
const port = Number(process.env.PORT ?? 44900);
|
|
14
13
|
|
|
14
|
+
const connConfig = {
|
|
15
|
+
host: process.env.QGRID_DB_HOST ?? "localhost",
|
|
16
|
+
port: Number(process.env.QGRID_DB_PORT ?? 44901),
|
|
17
|
+
user: process.env.QGRID_DB_USER ?? "postgres",
|
|
18
|
+
password: process.env.QGRID_DB_PASSWORD ?? "postgres",
|
|
19
|
+
database: process.env.QGRID_DB_NAME ?? "qgrid",
|
|
20
|
+
};
|
|
21
|
+
|
|
15
22
|
export default defineConfig({
|
|
16
23
|
projectName: process.env.PROJECT_NAME ?? "SonamuProject",
|
|
17
24
|
database: {
|
|
18
25
|
name: process.env.QGRID_DB_NAME ?? "qgrid",
|
|
19
26
|
defaultOptions: {
|
|
20
27
|
connection: {
|
|
21
|
-
|
|
22
|
-
port: Number(process.env.QGRID_DB_PORT ?? 44901),
|
|
23
|
-
user: process.env.QGRID_DB_USER ?? "postgres",
|
|
24
|
-
password: process.env.QGRID_DB_PASSWORD ?? "postgres",
|
|
28
|
+
...connConfig,
|
|
25
29
|
keepAlive: true,
|
|
26
30
|
keepAliveInitialDelayMillis: 10000,
|
|
27
31
|
},
|
|
@@ -85,11 +89,12 @@ export default defineConfig({
|
|
|
85
89
|
}),
|
|
86
90
|
},
|
|
87
91
|
loggers: [
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
// qgrid 로그 (요청 분배, oauth/usage API)
|
|
93
|
+
{ category: ["qgrid"], sinks: ["console"], lowestLevel: "info" },
|
|
94
|
+
// sonamu 로그는 warning 이상만
|
|
95
|
+
{ category: ["sonamu"], sinks: ["console"], lowestLevel: "warning" },
|
|
96
|
+
// fastify 로그 차단
|
|
97
|
+
{ category: ["fastify"], sinks: [], lowestLevel: "fatal" },
|
|
93
98
|
],
|
|
94
99
|
},
|
|
95
100
|
server: {
|
|
@@ -129,12 +134,7 @@ export default defineConfig({
|
|
|
129
134
|
body: request.body,
|
|
130
135
|
};
|
|
131
136
|
},
|
|
132
|
-
guardHandler: (
|
|
133
|
-
if (_guard === "user") {
|
|
134
|
-
console.log("user guard");
|
|
135
|
-
}
|
|
136
|
-
console.log("NOTHING YET");
|
|
137
|
-
},
|
|
137
|
+
guardHandler: () => {},
|
|
138
138
|
cacheControlHandler: (req) => {
|
|
139
139
|
switch (req.type) {
|
|
140
140
|
case "assets":
|
|
@@ -168,31 +168,6 @@ export default defineConfig({
|
|
|
168
168
|
}
|
|
169
169
|
},
|
|
170
170
|
},
|
|
171
|
-
storage: {
|
|
172
|
-
drivers: {
|
|
173
|
-
fs: drivers.fs({
|
|
174
|
-
location: path.join(import.meta.dirname, "/../public/uploaded"),
|
|
175
|
-
visibility: "public",
|
|
176
|
-
urlBuilder: {
|
|
177
|
-
async generateURL(key) {
|
|
178
|
-
return `/api/public/uploaded/${key}`;
|
|
179
|
-
},
|
|
180
|
-
async generateSignedURL(key) {
|
|
181
|
-
return `/api/public/uploaded/${key}`;
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
}),
|
|
185
|
-
s3: drivers.s3({
|
|
186
|
-
credentials: {
|
|
187
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "",
|
|
188
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "",
|
|
189
|
-
},
|
|
190
|
-
region: process.env.S3_REGION ?? "ap-northeast-2",
|
|
191
|
-
bucket: process.env.S3_BUCKET ?? "sonamu_default_bucket",
|
|
192
|
-
visibility: "private",
|
|
193
|
-
}),
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
171
|
cache: {
|
|
197
172
|
default: "main",
|
|
198
173
|
stores: {
|
|
@@ -220,19 +195,34 @@ export default defineConfig({
|
|
|
220
195
|
}
|
|
221
196
|
|
|
222
197
|
const { QgridDispatcher } = await import("./application/qgrid/qgrid.dispatcher");
|
|
198
|
+
const { ensureTokensTrigger } = await import("./application/qgrid/token-trigger-setup");
|
|
199
|
+
const { TokenSubscriber } = await import("./application/qgrid/token-subscriber");
|
|
200
|
+
|
|
201
|
+
let triggerReady = true;
|
|
223
202
|
try {
|
|
224
|
-
|
|
225
|
-
const entries = await TokenModel.findActive("A");
|
|
226
|
-
for (const e of entries) QgridDispatcher.addToken(e.token, e.name);
|
|
227
|
-
console.log(
|
|
228
|
-
`🌲 Server listening on http://${host}:${port} (${QgridDispatcher.tokens.size} tokens loaded)`,
|
|
229
|
-
);
|
|
203
|
+
await ensureTokensTrigger(connConfig);
|
|
230
204
|
} catch (e) {
|
|
231
|
-
|
|
232
|
-
console.
|
|
205
|
+
triggerReady = false;
|
|
206
|
+
console.warn(
|
|
207
|
+
`⚠ Token trigger setup failed: ${(e as Error).message}. Continuing with LISTEN/reconcile.`,
|
|
208
|
+
);
|
|
233
209
|
}
|
|
210
|
+
|
|
211
|
+
const subscriber = new TokenSubscriber(connConfig, QgridDispatcher);
|
|
212
|
+
QgridDispatcher.subscriber = subscriber;
|
|
213
|
+
|
|
214
|
+
const started = await subscriber.start();
|
|
215
|
+
const tokenInfo = started
|
|
216
|
+
? `${QgridDispatcher.tokens.size} active tokens, LISTEN active`
|
|
217
|
+
: `degraded — token sync retry scheduled`;
|
|
218
|
+
const triggerInfo = triggerReady ? "" : ", trigger setup failed";
|
|
219
|
+
console.log(`🌲 Server listening on http://${host}:${port} (${tokenInfo}${triggerInfo})`);
|
|
234
220
|
},
|
|
235
|
-
onShutdown: () => {
|
|
221
|
+
onShutdown: async () => {
|
|
222
|
+
const { QgridDispatcher } = await import("./application/qgrid/qgrid.dispatcher");
|
|
223
|
+
if (QgridDispatcher.subscriber) {
|
|
224
|
+
await QgridDispatcher.subscriber.stop();
|
|
225
|
+
}
|
|
236
226
|
console.log("graceful shutdown");
|
|
237
227
|
},
|
|
238
228
|
onError: (error, _request, reply) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** biome-ignore-all lint/correctness/noUnusedImports: d.ts */
|
|
2
2
|
import type fastify from "fastify";
|
|
3
3
|
|
|
4
|
-
import type
|
|
4
|
+
import { type UserSubsetSS } from "../application/sonamu.generated";
|
|
5
5
|
|
|
6
6
|
declare module "fastify" {
|
|
7
7
|
export interface PassportUser extends UserSubsetSS {}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** biome-ignore-all lint/correctness/noUnusedImports: d.ts */
|
|
2
|
-
|
|
3
|
-
import {} from "sonamu";
|
|
2
|
+
/* oxlint-disable no-empty-imports */
|
|
4
3
|
|
|
5
4
|
declare module "sonamu" {
|
|
6
5
|
export interface ContextExtend {
|
|
@@ -15,3 +14,5 @@ declare module "sonamu" {
|
|
|
15
14
|
// CustomGuardKey: true
|
|
16
15
|
}
|
|
17
16
|
}
|
|
17
|
+
|
|
18
|
+
export {};
|