tina4-nodejs 3.13.63 → 3.13.66
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/CLAUDE.md +3 -3
- package/package.json +1 -1
- package/packages/cli/src/bin.ts +338 -152
- package/packages/cli/src/commands/build.ts +110 -0
- package/packages/cli/src/commands/generate.ts +1154 -134
- package/packages/cli/src/commands/queue.ts +325 -0
- package/packages/core/public/js/frond.js +19 -8
- package/packages/core/public/js/frond.min.js +1 -1
- package/packages/core/src/routeDiscovery.ts +10 -2
- package/packages/core/src/types.ts +9 -1
- package/packages/orm/src/baseModel.ts +4 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: queue — run queue workers and manage jobs.
|
|
3
|
+
*
|
|
4
|
+
* The top-level `queue` command wires straight to the real @tina4/core Queue
|
|
5
|
+
* (file backend by default; RabbitMQ/Kafka/MongoDB via TINA4_QUEUE_BACKEND).
|
|
6
|
+
* `stats`, `retry` and `clear` operate on the queue without booting the app or a
|
|
7
|
+
* database; `work` runs the app's consumer for a topic. Distinct from
|
|
8
|
+
* `generate queue`, which SCAFFOLDS a consumer file.
|
|
9
|
+
*
|
|
10
|
+
* tina4nodejs queue work [topic] [--once] [--poll N] [--services DIR]
|
|
11
|
+
* tina4nodejs queue stats [topic] [--json]
|
|
12
|
+
* tina4nodejs queue retry [topic]
|
|
13
|
+
* tina4nodejs queue clear [status] [topic]
|
|
14
|
+
*
|
|
15
|
+
* Mirrors the Python master's _queue* handlers (tina4_python/cli/__init__.py).
|
|
16
|
+
*/
|
|
17
|
+
import { readdirSync, statSync } from "node:fs";
|
|
18
|
+
import { extname, join } from "node:path";
|
|
19
|
+
import { pathToFileURL } from "node:url";
|
|
20
|
+
import { loadEnv } from "../../../core/src/dotenv.js";
|
|
21
|
+
import { Queue } from "../../../core/src/queue.js";
|
|
22
|
+
import type { QueueJob } from "../../../core/src/job.js";
|
|
23
|
+
|
|
24
|
+
// ── Flag parsing ─────────────────────────────────────────────────────
|
|
25
|
+
//
|
|
26
|
+
// --key value and bare --flag, mirroring the Python master's _parse_flags:
|
|
27
|
+
// `once` and `json` are boolean-only (they never swallow the next token).
|
|
28
|
+
|
|
29
|
+
const BOOLEAN_FLAGS = new Set(["once", "json"]);
|
|
30
|
+
|
|
31
|
+
interface ParsedFlags {
|
|
32
|
+
flags: Record<string, string | boolean>;
|
|
33
|
+
positional: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseFlags(args: string[]): ParsedFlags {
|
|
37
|
+
const flags: Record<string, string | boolean> = {};
|
|
38
|
+
const positional: string[] = [];
|
|
39
|
+
let i = 0;
|
|
40
|
+
while (i < args.length) {
|
|
41
|
+
const arg = args[i];
|
|
42
|
+
if (arg.startsWith("--")) {
|
|
43
|
+
const key = arg.slice(2);
|
|
44
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
45
|
+
flags[key] = true;
|
|
46
|
+
i += 1;
|
|
47
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
48
|
+
flags[key] = args[i + 1];
|
|
49
|
+
i += 2;
|
|
50
|
+
} else {
|
|
51
|
+
flags[key] = true;
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
positional.push(arg);
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { flags, positional };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A per-job handler declared by a consumer module (receives the job payload). */
|
|
63
|
+
export type QueueHandler = (payload: unknown) => unknown | Promise<unknown>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Return the per-job handler that a consumer module declares for `topic`.
|
|
67
|
+
*
|
|
68
|
+
* A consumer module (e.g. the one `generate queue <topic>` scaffolds) exposes a
|
|
69
|
+
* default-export config; when its `topic` matches, `queue work` drives the
|
|
70
|
+
* consumer through that config's per-job `handle` callable — so the worker owns
|
|
71
|
+
* the poll loop (honouring --poll and the bounded --once drain) instead of the
|
|
72
|
+
* consumer's own endless loop. Returns the callable, or null when no consumer in
|
|
73
|
+
* `servicesDir` targets this topic. Mirrors Python's _resolve_queue_handler.
|
|
74
|
+
*/
|
|
75
|
+
export async function resolveQueueHandler(
|
|
76
|
+
servicesDir: string,
|
|
77
|
+
topic: string,
|
|
78
|
+
): Promise<QueueHandler | null> {
|
|
79
|
+
let entries: string[];
|
|
80
|
+
try {
|
|
81
|
+
if (!statSync(servicesDir).isDirectory()) return null;
|
|
82
|
+
entries = readdirSync(servicesDir);
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const entry of entries.sort()) {
|
|
88
|
+
if (entry.startsWith("_")) continue;
|
|
89
|
+
const ext = extname(entry);
|
|
90
|
+
if (ext !== ".ts" && ext !== ".js") continue;
|
|
91
|
+
|
|
92
|
+
const fullPath = join(servicesDir, entry);
|
|
93
|
+
try {
|
|
94
|
+
if (!statSync(fullPath).isFile()) continue;
|
|
95
|
+
const mod = await import(pathToFileURL(fullPath).href);
|
|
96
|
+
const config = (mod.default ?? mod) as Record<string, unknown> | undefined;
|
|
97
|
+
if (
|
|
98
|
+
config &&
|
|
99
|
+
typeof config === "object" &&
|
|
100
|
+
config.topic === topic &&
|
|
101
|
+
typeof config.handle === "function"
|
|
102
|
+
) {
|
|
103
|
+
return config.handle as QueueHandler;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// A broken sibling must not sink the worker — skip and keep scanning.
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The single available job out of a possible batch yield. */
|
|
114
|
+
function firstJob(yielded: QueueJob | QueueJob[]): QueueJob {
|
|
115
|
+
return Array.isArray(yielded) ? yielded[0] : yielded;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Subcommands ──────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Run a consumer loop that pops and processes jobs on a topic.
|
|
122
|
+
*
|
|
123
|
+
* tina4nodejs queue work [topic] [--once] [--poll N] [--services DIR]
|
|
124
|
+
*
|
|
125
|
+
* Long-running by default (polls every --poll seconds, 1.0 default; Ctrl-C to
|
|
126
|
+
* stop). --once does a single-pass drain — it processes every currently
|
|
127
|
+
* available job then exits (poll interval 0). The per-job handler is resolved
|
|
128
|
+
* from the app's consumer for this topic (see resolveQueueHandler); with no
|
|
129
|
+
* handler it drains and acks with a warning rather than inventing behaviour.
|
|
130
|
+
*/
|
|
131
|
+
async function queueWork(args: string[]): Promise<void> {
|
|
132
|
+
loadEnv();
|
|
133
|
+
const { flags, positional } = parseFlags(args);
|
|
134
|
+
const topic = positional[0] ?? "default";
|
|
135
|
+
const once = Boolean(flags.once);
|
|
136
|
+
|
|
137
|
+
// --poll is in SECONDS (parity with the Python master); consume() takes ms.
|
|
138
|
+
let pollSeconds: number;
|
|
139
|
+
if (once) {
|
|
140
|
+
pollSeconds = 0; // single-pass: consume() returns as soon as the topic is empty
|
|
141
|
+
} else {
|
|
142
|
+
const pollRaw = typeof flags.poll === "string" ? flags.poll.trim() : "";
|
|
143
|
+
const parsed = Number(pollRaw);
|
|
144
|
+
pollSeconds = pollRaw && Number.isFinite(parsed) ? parsed : 1.0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let servicesDir =
|
|
148
|
+
typeof flags.services === "string" && flags.services
|
|
149
|
+
? flags.services
|
|
150
|
+
: process.env.TINA4_SERVICE_DIR || "src/services";
|
|
151
|
+
if (flags.services === true) servicesDir = process.env.TINA4_SERVICE_DIR || "src/services";
|
|
152
|
+
|
|
153
|
+
const handler = await resolveQueueHandler(servicesDir, topic);
|
|
154
|
+
const queue = new Queue({ topic });
|
|
155
|
+
|
|
156
|
+
if (handler === null) {
|
|
157
|
+
console.log(` ⚠ No consumer handler found for topic '${topic}' in ${servicesDir}.`);
|
|
158
|
+
console.log(` Scaffold one with: tina4nodejs generate queue ${topic}`);
|
|
159
|
+
console.log(" Draining (consume + ack) without processing.");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const mode = once ? "single-pass drain" : `polling every ${pollSeconds}s (Ctrl-C to stop)`;
|
|
163
|
+
console.log(` Queue worker on '${topic}' — ${mode}...`);
|
|
164
|
+
|
|
165
|
+
let processed = 0;
|
|
166
|
+
let failed = 0;
|
|
167
|
+
|
|
168
|
+
// Long-running mode: Ctrl-C prints the tally and exits cleanly (the poll loop
|
|
169
|
+
// is idle in setTimeout when interrupted). --once is bounded, so no handler.
|
|
170
|
+
let onSigint: (() => void) | undefined;
|
|
171
|
+
if (!once) {
|
|
172
|
+
onSigint = () => {
|
|
173
|
+
console.log("\n Interrupted — stopping worker.");
|
|
174
|
+
console.log(` Processed ${processed} job(s), ${failed} failed on '${topic}'.`);
|
|
175
|
+
process.exit(0);
|
|
176
|
+
};
|
|
177
|
+
process.on("SIGINT", onSigint);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
for await (const yielded of queue.consume(topic, undefined, pollSeconds * 1000)) {
|
|
182
|
+
const job = firstJob(yielded);
|
|
183
|
+
try {
|
|
184
|
+
if (handler !== null) await handler(job.payload);
|
|
185
|
+
job.complete();
|
|
186
|
+
processed += 1;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
job.fail(String(err instanceof Error ? err.message : err));
|
|
189
|
+
failed += 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} finally {
|
|
193
|
+
if (onSigint) process.off("SIGINT", onSigint);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(` Processed ${processed} job(s), ${failed} failed on '${topic}'.`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Print pending / in-flight / failed / dead-letter / completed counts.
|
|
201
|
+
*
|
|
202
|
+
* tina4nodejs queue stats [topic] [--json]
|
|
203
|
+
*/
|
|
204
|
+
async function queueStats(args: string[]): Promise<void> {
|
|
205
|
+
loadEnv();
|
|
206
|
+
const { flags, positional } = parseFlags(args);
|
|
207
|
+
const topic = positional[0] ?? "default";
|
|
208
|
+
|
|
209
|
+
const queue = new Queue({ topic });
|
|
210
|
+
const stats = {
|
|
211
|
+
topic,
|
|
212
|
+
pending: queue.size("pending"), // waiting to run
|
|
213
|
+
reserved: queue.size("reserved"), // popped, not yet acked (in-flight)
|
|
214
|
+
failed: queue.failed().length, // failed once, still retrying
|
|
215
|
+
dead: queue.size("dead"), // exhausted retries (dead-letter)
|
|
216
|
+
completed: queue.size("completed"), // terminal-completed (0 on the file backend)
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (flags.json) {
|
|
220
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(`\n Queue '${topic}'`);
|
|
225
|
+
console.log(` pending ${stats.pending}`);
|
|
226
|
+
console.log(` reserved ${stats.reserved} (in-flight)`);
|
|
227
|
+
console.log(` failed ${stats.failed} (retrying)`);
|
|
228
|
+
console.log(` dead ${stats.dead} (dead-letter)`);
|
|
229
|
+
console.log(` completed ${stats.completed}`);
|
|
230
|
+
console.log("");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Re-queue failed and dead-letter jobs so they run again.
|
|
235
|
+
*
|
|
236
|
+
* tina4nodejs queue retry [topic]
|
|
237
|
+
*
|
|
238
|
+
* Revives every dead-letter job (manual override, regardless of attempt count)
|
|
239
|
+
* and re-queues any failed-but-still-eligible jobs.
|
|
240
|
+
*/
|
|
241
|
+
async function queueRetry(args: string[]): Promise<void> {
|
|
242
|
+
loadEnv();
|
|
243
|
+
const { positional } = parseFlags(args);
|
|
244
|
+
const topic = positional[0] ?? "default";
|
|
245
|
+
|
|
246
|
+
const queue = new Queue({ topic });
|
|
247
|
+
|
|
248
|
+
// maxRetries=0 => every job in the dead-letter store, whatever its attempt
|
|
249
|
+
// count (matches what stats/size("dead") reports), not only attempts>=N.
|
|
250
|
+
const dead = queue.deadLetters(0);
|
|
251
|
+
let revived = 0;
|
|
252
|
+
for (const job of dead) {
|
|
253
|
+
if (queue.retry(job.id)) revived += 1;
|
|
254
|
+
}
|
|
255
|
+
// Any failed-but-retryable jobs still under the limit (no-op on the file
|
|
256
|
+
// backend once the above moved them out, meaningful for other backends).
|
|
257
|
+
const requeued = queue.retryFailed();
|
|
258
|
+
|
|
259
|
+
const total = revived + requeued;
|
|
260
|
+
console.log(
|
|
261
|
+
` Re-queued ${total} job(s) on '${topic}' (${revived} dead-letter, ${requeued} failed).`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Purge jobs of a given status (default: completed).
|
|
267
|
+
*
|
|
268
|
+
* tina4nodejs queue clear [status] [topic]
|
|
269
|
+
*
|
|
270
|
+
* status is one of pending / reserved / completed / failed / dead. The default
|
|
271
|
+
* 'completed' clears finished jobs; pass e.g. `queue clear pending` or
|
|
272
|
+
* `queue clear dead orders` to purge another status / topic.
|
|
273
|
+
*/
|
|
274
|
+
async function queueClear(args: string[]): Promise<void> {
|
|
275
|
+
loadEnv();
|
|
276
|
+
const { positional } = parseFlags(args);
|
|
277
|
+
const status = positional[0] ?? "completed";
|
|
278
|
+
const topic = positional[1] ?? "default";
|
|
279
|
+
|
|
280
|
+
const queue = new Queue({ topic });
|
|
281
|
+
const removed = queue.purge(status);
|
|
282
|
+
console.log(` Cleared ${removed} '${status}' job(s) from '${topic}'.`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Sub-dispatch table ───────────────────────────────────────────────
|
|
286
|
+
//
|
|
287
|
+
// The single source for the queue subcommands — drives dispatch AND the
|
|
288
|
+
// manifest's queue.subcommands (bin.ts COMMANDS). Mirrors Python's
|
|
289
|
+
// _QUEUE_SUBCOMMANDS.
|
|
290
|
+
|
|
291
|
+
const QUEUE_SUBCOMMANDS: Record<string, (args: string[]) => Promise<void>> = {
|
|
292
|
+
work: queueWork,
|
|
293
|
+
stats: queueStats,
|
|
294
|
+
retry: queueRetry,
|
|
295
|
+
clear: queueClear,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/** Subcommand names, in order — surfaced in `commands --json` for the tina4 client. */
|
|
299
|
+
export const QUEUE_SUBCOMMAND_NAMES = Object.keys(QUEUE_SUBCOMMANDS);
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Top-level queue command: run workers and manage jobs. Dispatches to the
|
|
303
|
+
* subcommand handlers above; unknown / missing subcommands fail loud (exit 1).
|
|
304
|
+
*/
|
|
305
|
+
export async function queueCommand(args: string[] = []): Promise<void> {
|
|
306
|
+
const list = QUEUE_SUBCOMMAND_NAMES.join(", ");
|
|
307
|
+
|
|
308
|
+
if (!args || args.length === 0) {
|
|
309
|
+
console.log("Usage: tina4nodejs queue <work|stats|retry|clear> [options]");
|
|
310
|
+
console.log(` Subcommands: ${list}`);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const sub = args[0].toLowerCase();
|
|
316
|
+
const handler = QUEUE_SUBCOMMANDS[sub];
|
|
317
|
+
if (!handler) {
|
|
318
|
+
console.log(`Unknown queue subcommand: ${sub}`);
|
|
319
|
+
console.log(` Available: ${list}`);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
await handler(args.slice(1));
|
|
325
|
+
}
|
|
@@ -81,6 +81,12 @@ var _frondModule = (() => {
|
|
|
81
81
|
xhr.onerror = function() {
|
|
82
82
|
if (opts.onError) opts.onError(xhr.status, xhr);
|
|
83
83
|
};
|
|
84
|
+
if (opts.timeout !== 0) {
|
|
85
|
+
xhr.timeout = opts.timeout || 3e4;
|
|
86
|
+
xhr.ontimeout = function() {
|
|
87
|
+
if (opts.onError) opts.onError(xhr.status, xhr);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
84
90
|
xhr.send(body);
|
|
85
91
|
}
|
|
86
92
|
function inject(html, target) {
|
|
@@ -123,7 +129,7 @@ var _frondModule = (() => {
|
|
|
123
129
|
});
|
|
124
130
|
return body.innerHTML;
|
|
125
131
|
}
|
|
126
|
-
function load(url, target, callback) {
|
|
132
|
+
function load(url, target, callback, onError) {
|
|
127
133
|
const targetId = target || "content";
|
|
128
134
|
request(url, {
|
|
129
135
|
method: "GET",
|
|
@@ -134,10 +140,11 @@ var _frondModule = (() => {
|
|
|
134
140
|
} else {
|
|
135
141
|
if (callback) callback(data);
|
|
136
142
|
}
|
|
137
|
-
}
|
|
143
|
+
},
|
|
144
|
+
onError
|
|
138
145
|
});
|
|
139
146
|
}
|
|
140
|
-
function post(url, data, target, callback) {
|
|
147
|
+
function post(url, data, target, callback, onError) {
|
|
141
148
|
const targetId = target || "content";
|
|
142
149
|
request(url, {
|
|
143
150
|
method: "POST",
|
|
@@ -153,7 +160,8 @@ var _frondModule = (() => {
|
|
|
153
160
|
return;
|
|
154
161
|
}
|
|
155
162
|
if (callback) callback(html, responseData);
|
|
156
|
-
}
|
|
163
|
+
},
|
|
164
|
+
onError
|
|
157
165
|
});
|
|
158
166
|
}
|
|
159
167
|
var form = {
|
|
@@ -209,10 +217,11 @@ var _frondModule = (() => {
|
|
|
209
217
|
* @param url - URL to POST to.
|
|
210
218
|
* @param target - DOM id to inject response into (default: "message").
|
|
211
219
|
* @param callback - Optional callback.
|
|
220
|
+
* @param onError - Optional error callback — fires on non-2xx, transport error, or timeout.
|
|
212
221
|
*/
|
|
213
|
-
submit: function(formId, url, target, callback) {
|
|
222
|
+
submit: function(formId, url, target, callback, onError) {
|
|
214
223
|
const data = form.collect(formId);
|
|
215
|
-
post(url, data, target || "message", callback);
|
|
224
|
+
post(url, data, target || "message", callback, onError);
|
|
216
225
|
},
|
|
217
226
|
/**
|
|
218
227
|
* Load a form via the given action and inject response HTML.
|
|
@@ -224,8 +233,9 @@ var _frondModule = (() => {
|
|
|
224
233
|
* @param url - URL to fetch.
|
|
225
234
|
* @param target - DOM id to inject into (default: "form").
|
|
226
235
|
* @param callback - Optional callback.
|
|
236
|
+
* @param onError - Optional error callback — fires on non-2xx, transport error, or timeout.
|
|
227
237
|
*/
|
|
228
|
-
show: function(action, url, target, callback) {
|
|
238
|
+
show: function(action, url, target, callback, onError) {
|
|
229
239
|
let method = action.toUpperCase();
|
|
230
240
|
if (action === "create" || action === "edit") method = "GET";
|
|
231
241
|
if (action === "delete") method = "DELETE";
|
|
@@ -243,7 +253,8 @@ var _frondModule = (() => {
|
|
|
243
253
|
return;
|
|
244
254
|
}
|
|
245
255
|
if (callback) callback(html);
|
|
246
|
-
}
|
|
256
|
+
},
|
|
257
|
+
onError
|
|
247
258
|
});
|
|
248
259
|
}
|
|
249
260
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var _frondModule=(()=>{var E=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var H=Object.prototype.hasOwnProperty;var q=(t,s)=>{for(var e in s)E(t,e,{get:s[e],enumerable:!0})},D=(t,s,e,o)=>{if(s&&typeof s=="object"||typeof s=="function")for(let n of A(s))!H.call(t,n)&&n!==e&&E(t,n,{get:()=>s[n],enumerable:!(o=O(s,n))||o.enumerable});return t};var W=t=>D(E({},"__esModule",{value:!0}),t);var z={};q(z,{frond:()=>L});var y=null;function v(t,s){let e;typeof s=="function"?e={onSuccess:s}:e=s||{};let o=(e.method||"GET").toUpperCase(),n=new XMLHttpRequest;if(n.open(o,t,!0),y!==null&&n.setRequestHeader("Authorization","Bearer "+y),e.headers)for(let u in e.headers)Object.prototype.hasOwnProperty.call(e.headers,u)&&n.setRequestHeader(u,e.headers[u]);let c=null;e.body!==void 0&&e.body!==null&&(e.body instanceof FormData?c=e.body:typeof e.body=="object"?(c=JSON.stringify(e.body),n.setRequestHeader("Content-Type","application/json; charset=UTF-8")):typeof e.body=="string"&&(c=e.body,n.setRequestHeader("Content-Type","text/plain; charset=UTF-8"))),n.onload=function(){let u=n.getResponseHeader("FreshToken");u&&u!==""&&(y=u);let r=n.response;try{r=JSON.parse(r)}catch{}if(n.responseURL){let i=new URL(t,window.location.href).href;if(n.responseURL!==i){window.location.href=n.responseURL;return}}n.status>=200&&n.status<400?e.onSuccess&&e.onSuccess(r,n.status,n):e.onError&&e.onError(n.status,n)},n.onerror=function(){e.onError&&e.onError(n.status,n)},n.send(c)}function h(t,s){if(!t)return"";let e=new DOMParser,o=t.includes("<html>")?t:"<body>"+t+"</body></html>",c=e.parseFromString(o,"text/html").querySelector("body"),u=c.querySelectorAll("script");if(u.forEach(function(r){r.remove()}),s!==null){let r=document.getElementById(s);return r&&(c.children.length>0?r.replaceChildren.apply(r,Array.from(c.children)):r.innerHTML=c.innerHTML,u.forEach(function(i){let l=document.createElement("script");l.type="text/javascript",l.async=!0,i.src?l.src=i.src:l.textContent=i.textContent,r.appendChild(l)})),""}return u.forEach(function(r){let i=document.createElement("script");i.type="text/javascript",i.async=!0,i.textContent=r.textContent,document.body.appendChild(i)}),c.innerHTML}function _(t,s,e){let o=s||"content";v(t,{method:"GET",onSuccess:function(n,c){if(document.getElementById(o)){let u=h(n,o);e&&e(u,n)}else e&&e(n)}})}function R(t,s,e,o){let n=e||"content";v(t,{method:"POST",body:s,onSuccess:function(c){let u="";if(c&&c.message!==void 0)u=h(c.message,n);else if(document.getElementById(n))u=h(c,n);else{o&&o(c);return}o&&o(u,c)}})}var x={collect:function(t){let s=new FormData,e=document.querySelectorAll("#"+t+" select, #"+t+" input, #"+t+" textarea");for(let o=0;o<e.length;o++){let n=e[o];if(n.name==="formToken"&&y!==null&&(n.value=y),!!n.name)if(n.type==="file"){let c=n.files;if(c)for(let u=0;u<c.length;u++){let r=c[u];if(r!==void 0){let i=n.name;c.length>1&&!i.includes("[")&&(i=i+"[]"),s.append(i,r,r.name)}}}else n.type==="checkbox"||n.type==="radio"?n.checked?s.append(n.name,n.value):n.type!=="radio"&&s.append(n.name,"0"):s.append(n.name,n.value===""?"":n.value)}return s},submit:function(t,s,e,o){let n=x.collect(t);R(s,n,e||"message",o)},show:function(t,s,e,o){let n=t.toUpperCase();(t==="create"||t==="edit")&&(n="GET"),t==="delete"&&(n="DELETE");let c=e||"form";v(s,{method:n,onSuccess:function(u){let r="";if(u&&u.message!==void 0)r=h(u.message,c);else if(document.getElementById(c))r=h(u,c);else{o&&o(u);return}o&&o(r)}})}};function M(t,s){let e={reconnect:!0,reconnectDelay:1e3,maxReconnectDelay:3e4,maxReconnectAttempts:1/0,protocols:[],onOpen:function(){},onClose:function(){},onError:function(){},...s||{}},o=null,n=!1,c=e.reconnectDelay,u=0,r=null,i={message:[],open:[],close:[],error:[]},l={status:"connecting",send:function(f){if(!o||o.readyState!==WebSocket.OPEN)throw new Error("[frond] WebSocket is not connected");o.send(typeof f=="string"?f:JSON.stringify(f))},on:function(f,a){return i[f]||(i[f]=[]),i[f].push(a),function(){let d=i[f],g=d.indexOf(a);g>=0&&d.splice(g,1)}},close:function(f,a){n=!0,r&&(clearTimeout(r),r=null),o&&o.close(f||1e3,a||""),l.status="closed"}};function p(f){if(typeof f!="string")return f;try{return JSON.parse(f)}catch{return f}}function m(){!e.reconnect||u>=e.maxReconnectAttempts||(u++,l.status="reconnecting",r=setTimeout(function(){r=null,w()},c),c=Math.min(c*2,e.maxReconnectDelay))}function w(){l.status=u>0?"reconnecting":"connecting";try{o=new WebSocket(t,e.protocols)}catch{l.status="closed";return}o.onopen=function(){l.status="open",u=0,c=e.reconnectDelay,e.onOpen();for(let f of i.open)f()},o.onmessage=function(f){let a=p(f.data);for(let d of i.message)d(a)},o.onclose=function(f){l.status="closed",e.onClose(f.code,f.reason);for(let a of i.close)a(f.code,f.reason);n||m()},o.onerror=function(f){e.onError(f);for(let a of i.error)a(f)}}return w(),l}function I(t,s){let e={reconnect:!0,reconnectDelay:1e3,maxReconnectDelay:3e4,maxReconnectAttempts:1/0,events:[],json:!0,onOpen:function(){},onClose:function(){},onError:function(){},...s||{}},o=null,n=!1,c=e.reconnectDelay,u=0,r=null,i={message:[],open:[],close:[],error:[]},l={status:"connecting",on:function(a,d){return i[a]||(i[a]=[]),i[a].push(d),function(){let g=i[a],T=g.indexOf(d);T>=0&&g.splice(T,1)}},close:function(){n=!0,r&&(clearTimeout(r),r=null),o&&(o.close(),o=null),l.status="closed"}};function p(a){if(!e.json)return a;try{return JSON.parse(a)}catch{return a}}function m(a,d){for(let g of i.message)g(a,d||void 0)}function w(){!e.reconnect||u>=e.maxReconnectAttempts||(u++,l.status="reconnecting",r=setTimeout(function(){r=null,f()},c),c=Math.min(c*2,e.maxReconnectDelay))}function f(){l.status=u>0?"reconnecting":"connecting";try{o=new EventSource(t)}catch{l.status="closed";return}o.onopen=function(){l.status="open",u=0,c=e.reconnectDelay,e.onOpen();for(let a of i.open)a(null)},o.onmessage=function(a){m(p(a.data),null)};for(let a of e.events)o.addEventListener(a,function(d){m(p(d.data),a)});o.onerror=function(a){e.onError(a);for(let d of i.error)d(a);if(o&&o.readyState===2){o=null,l.status="closed",e.onClose();for(let d of i.close)d(null);n||w()}}}return f(),l}var U={set:function(t,s,e){let o="";if(e){let n=new Date;n.setTime(n.getTime()+e*24*60*60*1e3),o="; expires="+n.toUTCString()}document.cookie=t+"="+(s||"")+o+"; path=/"},get:function(t){let s=t+"=",e=document.cookie.split(";");for(let o=0;o<e.length;o++){let n=e[o];for(;n.charAt(0)===" ";)n=n.substring(1);if(n.indexOf(s)===0)return n.substring(s.length)}return null},remove:function(t){document.cookie=t+"=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/"}};function j(t,s){let e=document.getElementById("message");if(!e)return;let o=s||"info";e.innerHTML='<div class="alert alert-'+o+' alert-dismissible">'+t+'<button type="button" class="btn-close" data-t4-dismiss="alert">×</button></div>'}function B(t,s,e,o){let n=window.screenLeft!==void 0?window.screenLeft:window.screenX,c=window.screenTop!==void 0?window.screenTop:window.screenY,u=window.innerWidth||document.documentElement.clientWidth||screen.width,r=window.innerHeight||document.documentElement.clientHeight||screen.height,i=u/window.screen.availWidth,l=(u-e)/2/i+n,p=(r-o)/2/i+c,m=window.open(t,s,"directories=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width="+e/i+",height="+o/i+",top="+p+",left="+l);return window.focus&&m&&m.focus(),m}function P(t){if(t.indexOf("No data available")>=0){window.alert("No data available for this report.");return}window.open(t,"_blank","toolbar=no,scrollbars=yes,resizable=yes,width=800,height=600,top=0,left=0")}function F(t,s,e,o){v(t,{method:"POST",body:{query:s,variables:e||{}},onSuccess:function(n){o&&o(n.data||null,n.errors||void 0)},onError:function(n){o&&o(null,[{message:"GraphQL request failed with status "+n}])}})}function b(t){let s=t.dataset;return s&&s.key?s.key:null}function X(t,s){let e=s.attributes;for(let n=0;n<e.length;n++){let c=e[n];t.getAttribute(c.name)!==c.value&&t.setAttribute(c.name,c.value)}Array.prototype.slice.call(t.attributes).forEach(function(n){s.hasAttribute(n.name)||t.removeAttribute(n.name)})}function G(t,s){let e=s.tagName;e==="INPUT"||e==="TEXTAREA"||e==="SELECT"||(X(t,s),t.children.length||s.children.length?C(t,s):t.innerHTML!==s.innerHTML&&(t.innerHTML=s.innerHTML))}function C(t,s){let e=Array.prototype.slice.call(t.children),o=Array.prototype.slice.call(s.children),n={};e.forEach(function(r){let i=b(r);i&&(n[i]=r)});let c=[];for(let r=0;r<o.length;r++){let i=o[r],l=b(i),p=null;l&&n[l]?p=n[l]:!l&&e[r]&&!b(e[r])&&e[r].tagName===i.tagName&&(p=e[r]),p&&p.tagName===i.tagName?(G(p,i),reused.push(p),c.push(p)):c.push(i)}let u=t.firstElementChild;for(let r=0;r<c.length;r++){let i=c[r];i===u?u=u.nextElementSibling:t.insertBefore(i,u)}e.forEach(function(r){c.indexOf(r)===-1&&r.parentNode===t&&t.removeChild(r)}),reused}function k(t,s){let e=document.createElement("div");if(e.innerHTML=s,!e.children.length||!t.children.length){t.innerHTML=s;return}C(t,e)}function J(t){return/^wss?:\/\//.test(t)?t:(typeof location<"u"&&location.protocol==="https:"?"wss":"ws")+"://"+location.host+t}function N(t,s){return t&&typeof t=="object"?t.type==="live"?s&&t.name&&t.name!==s?null:t.html!=null?String(t.html):null:null:typeof t=="string"?t:null}function S(t){if(typeof document>"u")return;let e=(t||document).querySelectorAll("[data-frond-live]");Array.prototype.slice.call(e).forEach(function(o){if(o.__frondLive)return;o.__frondLive=!0;let n=o.getAttribute("data-mode"),c=o.getAttribute("data-frond-live");if(n==="poll"){let u=o.getAttribute("data-src"),r=(parseInt(o.getAttribute("data-interval"),10)||5)*1e3,i=setInterval(function(){typeof document<"u"&&document.hidden||v(u,{method:"GET",onSuccess:function(l){k(o,typeof l=="string"?l:String(l))}})},r);o.__frondLiveStop=function(){clearInterval(i)}}else if(n==="ws"){let u=M(J(o.getAttribute("data-ws")));u.on("message",function(r){let i=N(r,c);i!==null&&k(o,i)}),o.__frondLiveStop=function(){u.close()}}else n==="sse"&&typeof console<"u"&&console.warn&&console.warn("[frond.live] sse transport is not wired yet (v1 supports poll and ws); block '"+c+"' shows first paint only. Use poll or ws.")})}var L={request:v,load:_,post:R,inject:h,form:x,ws:M,sse:I,live:S,cookie:U,message:j,popup:B,report:P,graphql:F,get token(){return y},set token(t){y=t}};typeof window<"u"&&(window.frond=L,typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){S()}):S()));return W(z);})();
|
|
1
|
+
var _frondModule=(()=>{var E=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var q=Object.prototype.hasOwnProperty;var A=(t,s)=>{for(var e in s)E(t,e,{get:s[e],enumerable:!0})},D=(t,s,e,o)=>{if(s&&typeof s=="object"||typeof s=="function")for(let n of H(s))!q.call(t,n)&&n!==e&&E(t,n,{get:()=>s[n],enumerable:!(o=O(s,n))||o.enumerable});return t};var W=t=>D(E({},"__esModule",{value:!0}),t);var z={};A(z,{frond:()=>C});var y=null;function v(t,s){let e;typeof s=="function"?e={onSuccess:s}:e=s||{};let o=(e.method||"GET").toUpperCase(),n=new XMLHttpRequest;if(n.open(o,t,!0),y!==null&&n.setRequestHeader("Authorization","Bearer "+y),e.headers)for(let u in e.headers)Object.prototype.hasOwnProperty.call(e.headers,u)&&n.setRequestHeader(u,e.headers[u]);let c=null;e.body!==void 0&&e.body!==null&&(e.body instanceof FormData?c=e.body:typeof e.body=="object"?(c=JSON.stringify(e.body),n.setRequestHeader("Content-Type","application/json; charset=UTF-8")):typeof e.body=="string"&&(c=e.body,n.setRequestHeader("Content-Type","text/plain; charset=UTF-8"))),n.onload=function(){let u=n.getResponseHeader("FreshToken");u&&u!==""&&(y=u);let r=n.response;try{r=JSON.parse(r)}catch{}if(n.responseURL){let i=new URL(t,window.location.href).href;if(n.responseURL!==i){window.location.href=n.responseURL;return}}n.status>=200&&n.status<400?e.onSuccess&&e.onSuccess(r,n.status,n):e.onError&&e.onError(n.status,n)},n.onerror=function(){e.onError&&e.onError(n.status,n)},e.timeout!==0&&(n.timeout=e.timeout||3e4,n.ontimeout=function(){e.onError&&e.onError(n.status,n)}),n.send(c)}function h(t,s){if(!t)return"";let e=new DOMParser,o=t.includes("<html>")?t:"<body>"+t+"</body></html>",c=e.parseFromString(o,"text/html").querySelector("body"),u=c.querySelectorAll("script");if(u.forEach(function(r){r.remove()}),s!==null){let r=document.getElementById(s);return r&&(c.children.length>0?r.replaceChildren.apply(r,Array.from(c.children)):r.innerHTML=c.innerHTML,u.forEach(function(i){let l=document.createElement("script");l.type="text/javascript",l.async=!0,i.src?l.src=i.src:l.textContent=i.textContent,r.appendChild(l)})),""}return u.forEach(function(r){let i=document.createElement("script");i.type="text/javascript",i.async=!0,i.textContent=r.textContent,document.body.appendChild(i)}),c.innerHTML}function _(t,s,e,o){let n=s||"content";v(t,{method:"GET",onSuccess:function(c,u){if(document.getElementById(n)){let r=h(c,n);e&&e(r,c)}else e&&e(c)},onError:o})}function k(t,s,e,o,n){let c=e||"content";v(t,{method:"POST",body:s,onSuccess:function(u){let r="";if(u&&u.message!==void 0)r=h(u.message,c);else if(document.getElementById(c))r=h(u,c);else{o&&o(u);return}o&&o(r,u)},onError:n})}var x={collect:function(t){let s=new FormData,e=document.querySelectorAll("#"+t+" select, #"+t+" input, #"+t+" textarea");for(let o=0;o<e.length;o++){let n=e[o];if(n.name==="formToken"&&y!==null&&(n.value=y),!!n.name)if(n.type==="file"){let c=n.files;if(c)for(let u=0;u<c.length;u++){let r=c[u];if(r!==void 0){let i=n.name;c.length>1&&!i.includes("[")&&(i=i+"[]"),s.append(i,r,r.name)}}}else n.type==="checkbox"||n.type==="radio"?n.checked?s.append(n.name,n.value):n.type!=="radio"&&s.append(n.name,"0"):s.append(n.name,n.value===""?"":n.value)}return s},submit:function(t,s,e,o,n){let c=x.collect(t);k(s,c,e||"message",o,n)},show:function(t,s,e,o,n){let c=t.toUpperCase();(t==="create"||t==="edit")&&(c="GET"),t==="delete"&&(c="DELETE");let u=e||"form";v(s,{method:c,onSuccess:function(r){let i="";if(r&&r.message!==void 0)i=h(r.message,u);else if(document.getElementById(u))i=h(r,u);else{o&&o(r);return}o&&o(i)},onError:n})}};function M(t,s){let e={reconnect:!0,reconnectDelay:1e3,maxReconnectDelay:3e4,maxReconnectAttempts:1/0,protocols:[],onOpen:function(){},onClose:function(){},onError:function(){},...s||{}},o=null,n=!1,c=e.reconnectDelay,u=0,r=null,i={message:[],open:[],close:[],error:[]},l={status:"connecting",send:function(f){if(!o||o.readyState!==WebSocket.OPEN)throw new Error("[frond] WebSocket is not connected");o.send(typeof f=="string"?f:JSON.stringify(f))},on:function(f,a){return i[f]||(i[f]=[]),i[f].push(a),function(){let d=i[f],g=d.indexOf(a);g>=0&&d.splice(g,1)}},close:function(f,a){n=!0,r&&(clearTimeout(r),r=null),o&&o.close(f||1e3,a||""),l.status="closed"}};function m(f){if(typeof f!="string")return f;try{return JSON.parse(f)}catch{return f}}function p(){!e.reconnect||u>=e.maxReconnectAttempts||(u++,l.status="reconnecting",r=setTimeout(function(){r=null,w()},c),c=Math.min(c*2,e.maxReconnectDelay))}function w(){l.status=u>0?"reconnecting":"connecting";try{o=new WebSocket(t,e.protocols)}catch{l.status="closed";return}o.onopen=function(){l.status="open",u=0,c=e.reconnectDelay,e.onOpen();for(let f of i.open)f()},o.onmessage=function(f){let a=m(f.data);for(let d of i.message)d(a)},o.onclose=function(f){l.status="closed",e.onClose(f.code,f.reason);for(let a of i.close)a(f.code,f.reason);n||p()},o.onerror=function(f){e.onError(f);for(let a of i.error)a(f)}}return w(),l}function I(t,s){let e={reconnect:!0,reconnectDelay:1e3,maxReconnectDelay:3e4,maxReconnectAttempts:1/0,events:[],json:!0,onOpen:function(){},onClose:function(){},onError:function(){},...s||{}},o=null,n=!1,c=e.reconnectDelay,u=0,r=null,i={message:[],open:[],close:[],error:[]},l={status:"connecting",on:function(a,d){return i[a]||(i[a]=[]),i[a].push(d),function(){let g=i[a],T=g.indexOf(d);T>=0&&g.splice(T,1)}},close:function(){n=!0,r&&(clearTimeout(r),r=null),o&&(o.close(),o=null),l.status="closed"}};function m(a){if(!e.json)return a;try{return JSON.parse(a)}catch{return a}}function p(a,d){for(let g of i.message)g(a,d||void 0)}function w(){!e.reconnect||u>=e.maxReconnectAttempts||(u++,l.status="reconnecting",r=setTimeout(function(){r=null,f()},c),c=Math.min(c*2,e.maxReconnectDelay))}function f(){l.status=u>0?"reconnecting":"connecting";try{o=new EventSource(t)}catch{l.status="closed";return}o.onopen=function(){l.status="open",u=0,c=e.reconnectDelay,e.onOpen();for(let a of i.open)a(null)},o.onmessage=function(a){p(m(a.data),null)};for(let a of e.events)o.addEventListener(a,function(d){p(m(d.data),a)});o.onerror=function(a){e.onError(a);for(let d of i.error)d(a);if(o&&o.readyState===2){o=null,l.status="closed",e.onClose();for(let d of i.close)d(null);n||w()}}}return f(),l}var U={set:function(t,s,e){let o="";if(e){let n=new Date;n.setTime(n.getTime()+e*24*60*60*1e3),o="; expires="+n.toUTCString()}document.cookie=t+"="+(s||"")+o+"; path=/"},get:function(t){let s=t+"=",e=document.cookie.split(";");for(let o=0;o<e.length;o++){let n=e[o];for(;n.charAt(0)===" ";)n=n.substring(1);if(n.indexOf(s)===0)return n.substring(s.length)}return null},remove:function(t){document.cookie=t+"=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/"}};function j(t,s){let e=document.getElementById("message");if(!e)return;let o=s||"info";e.innerHTML='<div class="alert alert-'+o+' alert-dismissible">'+t+'<button type="button" class="btn-close" data-t4-dismiss="alert">×</button></div>'}function X(t,s,e,o){let n=window.screenLeft!==void 0?window.screenLeft:window.screenX,c=window.screenTop!==void 0?window.screenTop:window.screenY,u=window.innerWidth||document.documentElement.clientWidth||screen.width,r=window.innerHeight||document.documentElement.clientHeight||screen.height,i=u/window.screen.availWidth,l=(u-e)/2/i+n,m=(r-o)/2/i+c,p=window.open(t,s,"directories=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width="+e/i+",height="+o/i+",top="+m+",left="+l);return window.focus&&p&&p.focus(),p}function B(t){if(t.indexOf("No data available")>=0){window.alert("No data available for this report.");return}window.open(t,"_blank","toolbar=no,scrollbars=yes,resizable=yes,width=800,height=600,top=0,left=0")}function P(t,s,e,o){v(t,{method:"POST",body:{query:s,variables:e||{}},onSuccess:function(n){o&&o(n.data||null,n.errors||void 0)},onError:function(n){o&&o(null,[{message:"GraphQL request failed with status "+n}])}})}function b(t){let s=t.dataset;return s&&s.key?s.key:null}function F(t,s){let e=s.attributes;for(let n=0;n<e.length;n++){let c=e[n];t.getAttribute(c.name)!==c.value&&t.setAttribute(c.name,c.value)}Array.prototype.slice.call(t.attributes).forEach(function(n){s.hasAttribute(n.name)||t.removeAttribute(n.name)})}function G(t,s){let e=s.tagName;e==="INPUT"||e==="TEXTAREA"||e==="SELECT"||(F(t,s),t.children.length||s.children.length?L(t,s):t.innerHTML!==s.innerHTML&&(t.innerHTML=s.innerHTML))}function L(t,s){let e=Array.prototype.slice.call(t.children),o=Array.prototype.slice.call(s.children),n={};e.forEach(function(r){let i=b(r);i&&(n[i]=r)});let c=[];for(let r=0;r<o.length;r++){let i=o[r],l=b(i),m=null;l&&n[l]?m=n[l]:!l&&e[r]&&!b(e[r])&&e[r].tagName===i.tagName&&(m=e[r]),m&&m.tagName===i.tagName?(G(m,i),reused.push(m),c.push(m)):c.push(i)}let u=t.firstElementChild;for(let r=0;r<c.length;r++){let i=c[r];i===u?u=u.nextElementSibling:t.insertBefore(i,u)}e.forEach(function(r){c.indexOf(r)===-1&&r.parentNode===t&&t.removeChild(r)}),reused}function R(t,s){let e=document.createElement("div");if(e.innerHTML=s,!e.children.length||!t.children.length){t.innerHTML=s;return}L(t,e)}function J(t){return/^wss?:\/\//.test(t)?t:(typeof location<"u"&&location.protocol==="https:"?"wss":"ws")+"://"+location.host+t}function N(t,s){return t&&typeof t=="object"?t.type==="live"?s&&t.name&&t.name!==s?null:t.html!=null?String(t.html):null:null:typeof t=="string"?t:null}function S(t){if(typeof document>"u")return;let e=(t||document).querySelectorAll("[data-frond-live]");Array.prototype.slice.call(e).forEach(function(o){if(o.__frondLive)return;o.__frondLive=!0;let n=o.getAttribute("data-mode"),c=o.getAttribute("data-frond-live");if(n==="poll"){let u=o.getAttribute("data-src"),r=(parseInt(o.getAttribute("data-interval"),10)||5)*1e3,i=setInterval(function(){typeof document<"u"&&document.hidden||v(u,{method:"GET",onSuccess:function(l){R(o,typeof l=="string"?l:String(l))}})},r);o.__frondLiveStop=function(){clearInterval(i)}}else if(n==="ws"){let u=M(J(o.getAttribute("data-ws")));u.on("message",function(r){let i=N(r,c);i!==null&&R(o,i)}),o.__frondLiveStop=function(){u.close()}}else n==="sse"&&typeof console<"u"&&console.warn&&console.warn("[frond.live] sse transport is not wired yet (v1 supports poll and ws); block '"+c+"' shows first paint only. Use poll or ws.")})}var C={request:v,load:_,post:k,inject:h,form:x,ws:M,sse:I,live:S,cookie:U,message:j,popup:X,report:B,graphql:P,get token(){return y},set token(t){y=t}};typeof window<"u"&&(window.frond=C,typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){S()}):S()));return W(z);})();
|
|
2
2
|
/* Frond v2.2.0 - tina4.com */
|
|
@@ -64,8 +64,16 @@ export async function discoverRoutes(routesDir: string): Promise<RouteDefinition
|
|
|
64
64
|
|
|
65
65
|
const meta: RouteMeta | undefined = mod.meta;
|
|
66
66
|
const template: string | undefined = typeof mod.template === "string" ? mod.template : undefined;
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
// Auth opt-outs a route file can export (parity with the imperative
|
|
68
|
+
// `.noAuth()` / AutoCrud `secure: false`). A generated public write file
|
|
69
|
+
// (`generate route … --public`, or the always-public auth login/register)
|
|
70
|
+
// does `export const secure = false;`; without threading it here the
|
|
71
|
+
// router would keep its secure-by-default write gate and the opt-out
|
|
72
|
+
// would be inert. Only booleans are honoured — anything else is ignored.
|
|
73
|
+
const secure: boolean | undefined = typeof mod.secure === "boolean" ? mod.secure : undefined;
|
|
74
|
+
const noAuth: boolean | undefined = typeof mod.noAuth === "boolean" ? mod.noAuth : undefined;
|
|
75
|
+
|
|
76
|
+
definitions.push({ method, pattern, handler, filePath, meta, template, secure, noAuth });
|
|
69
77
|
_seenFiles.add(filePath);
|
|
70
78
|
_seenMtimes.set(filePath, currentMtime);
|
|
71
79
|
registeredFromThisScan++;
|
|
@@ -109,7 +109,15 @@ export type Tina4Response =
|
|
|
109
109
|
((data?: unknown, statusCode?: number, contentType?: string) => Tina4Response)
|
|
110
110
|
& Tina4ResponseMethods;
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
// A route handler may resolve `void` (it wrote to `res` directly) OR return the
|
|
113
|
+
// `Tina4Response` from `res.json(...)` / `response(...)` - the established
|
|
114
|
+
// convention across the framework's own routes, realtime handlers, and the
|
|
115
|
+
// `generate` scaffolding (`return res.json(...)`). The dispatcher tolerates a
|
|
116
|
+
// returned response; the type just has to allow it.
|
|
117
|
+
export type RouteHandler = (
|
|
118
|
+
req: Tina4Request,
|
|
119
|
+
res: Tina4Response,
|
|
120
|
+
) => Tina4Response | void | Promise<Tina4Response | void>;
|
|
113
121
|
|
|
114
122
|
export interface RouteDefinition {
|
|
115
123
|
method: string;
|
|
@@ -586,6 +586,7 @@ export class BaseModel {
|
|
|
586
586
|
* @param limit Max records (default 20)
|
|
587
587
|
* @param offset Skip records (default 0)
|
|
588
588
|
* @param include Relationship names to eager-load
|
|
589
|
+
* @param orderBy ORDER BY clause (e.g. "name ASC")
|
|
589
590
|
*/
|
|
590
591
|
static async where<T extends BaseModel>(
|
|
591
592
|
this: new (data?: Record<string, unknown>) => T,
|
|
@@ -594,6 +595,7 @@ export class BaseModel {
|
|
|
594
595
|
limit: number = 20,
|
|
595
596
|
offset: number = 0,
|
|
596
597
|
include?: string[],
|
|
598
|
+
orderBy?: string,
|
|
597
599
|
): Promise<T[]> {
|
|
598
600
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
599
601
|
const db = ModelClass.getDb();
|
|
@@ -607,7 +609,8 @@ export class BaseModel {
|
|
|
607
609
|
}
|
|
608
610
|
parts.push(`(${conditions})`);
|
|
609
611
|
|
|
610
|
-
const
|
|
612
|
+
const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
|
|
613
|
+
const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
|
|
611
614
|
|
|
612
615
|
const rows = await adapterQuery(db, sql, params);
|
|
613
616
|
const instances = rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
|