@tthr/vue 0.0.65 → 0.0.67
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/nuxt/runtime/server/plugins/cron.js +246 -10
- package/package.json +1 -1
|
@@ -13,6 +13,9 @@ import { defineNitroPlugin } from 'nitropack/runtime';
|
|
|
13
13
|
export { defineNitroPlugin };
|
|
14
14
|
// Store for registered cron handlers
|
|
15
15
|
const cronHandlers = new Map();
|
|
16
|
+
// Dynamic function registry - populated on first cron trigger
|
|
17
|
+
let functionRegistry = null;
|
|
18
|
+
let registryError = null;
|
|
16
19
|
// WebSocket connection state
|
|
17
20
|
let ws = null;
|
|
18
21
|
let connectionId = null;
|
|
@@ -69,6 +72,203 @@ export function unregisterCronHandler(functionName) {
|
|
|
69
72
|
export function getCronHandlers() {
|
|
70
73
|
return Array.from(cronHandlers.keys());
|
|
71
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Load user's custom functions from ~/tether/functions
|
|
77
|
+
*/
|
|
78
|
+
async function loadFunctionRegistry() {
|
|
79
|
+
if (functionRegistry !== null || registryError !== null) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
log.debug('Loading custom functions from ~~/tether/functions/index.ts');
|
|
84
|
+
const functions = await import('~~/tether/functions/index.ts').catch((err) => {
|
|
85
|
+
log.debug('Failed to import functions:', err.message);
|
|
86
|
+
return null;
|
|
87
|
+
});
|
|
88
|
+
if (functions) {
|
|
89
|
+
log.debug('Loaded function modules:', Object.keys(functions));
|
|
90
|
+
functionRegistry = functions;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
log.debug('No custom functions found');
|
|
94
|
+
functionRegistry = {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
log.debug('Could not load custom functions:', error instanceof Error ? error.message : String(error));
|
|
99
|
+
registryError = error instanceof Error ? error : new Error(String(error));
|
|
100
|
+
functionRegistry = {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Look up a function by name (e.g., "sync.syncClips")
|
|
105
|
+
*/
|
|
106
|
+
function lookupFunction(name) {
|
|
107
|
+
if (!functionRegistry || !name)
|
|
108
|
+
return null;
|
|
109
|
+
const parts = name.split('.');
|
|
110
|
+
if (parts.length !== 2)
|
|
111
|
+
return null;
|
|
112
|
+
const [moduleName, fnName] = parts;
|
|
113
|
+
const module = functionRegistry[moduleName];
|
|
114
|
+
if (!module)
|
|
115
|
+
return null;
|
|
116
|
+
const fn = module[fnName];
|
|
117
|
+
// Check if it's a valid Tether function (has a handler)
|
|
118
|
+
if (fn && typeof fn === 'object' && typeof fn.handler === 'function') {
|
|
119
|
+
return fn;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Create a database proxy that routes calls to Tether's CRUD endpoints
|
|
125
|
+
*/
|
|
126
|
+
function createDatabaseProxy(apiKey, url, projectId, environment) {
|
|
127
|
+
const base = url.replace(/\/$/, '');
|
|
128
|
+
let apiPath;
|
|
129
|
+
if (environment && environment !== 'production') {
|
|
130
|
+
apiPath = `${base}/api/v1/projects/${projectId}/env/${environment}`;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
apiPath = `${base}/api/v1/projects/${projectId}`;
|
|
134
|
+
}
|
|
135
|
+
return new Proxy({}, {
|
|
136
|
+
get(_target, tableName) {
|
|
137
|
+
const makeRequest = async (operation, args) => {
|
|
138
|
+
const response = await fetch(`${apiPath}/query`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: {
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
function: `${tableName}.${operation}`,
|
|
146
|
+
args,
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
151
|
+
throw new Error(error.error || `Database operation failed: ${tableName}.${operation}`);
|
|
152
|
+
}
|
|
153
|
+
const result = await response.json();
|
|
154
|
+
return result.data;
|
|
155
|
+
};
|
|
156
|
+
const makeMutation = async (operation, args) => {
|
|
157
|
+
const response = await fetch(`${apiPath}/mutation`, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
function: `${tableName}.${operation}`,
|
|
165
|
+
args,
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
170
|
+
throw new Error(error.error || `Database operation failed: ${tableName}.${operation}`);
|
|
171
|
+
}
|
|
172
|
+
const result = await response.json();
|
|
173
|
+
return result.data;
|
|
174
|
+
};
|
|
175
|
+
return {
|
|
176
|
+
findMany: async (options) => {
|
|
177
|
+
const args = {};
|
|
178
|
+
if (options?.where)
|
|
179
|
+
args.where = options.where;
|
|
180
|
+
if (options?.limit)
|
|
181
|
+
args.limit = options.limit;
|
|
182
|
+
if (options?.offset)
|
|
183
|
+
args.offset = options.offset;
|
|
184
|
+
if (options?.orderBy) {
|
|
185
|
+
if (typeof options.orderBy === 'string') {
|
|
186
|
+
args.orderBy = options.orderBy;
|
|
187
|
+
}
|
|
188
|
+
else if (typeof options.orderBy === 'object') {
|
|
189
|
+
const entries = Object.entries(options.orderBy);
|
|
190
|
+
if (entries.length > 0) {
|
|
191
|
+
const [column, dir] = entries[0];
|
|
192
|
+
args.orderBy = column;
|
|
193
|
+
args.orderDir = dir?.toUpperCase?.() || 'ASC';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (options?.orderDir)
|
|
198
|
+
args.orderDir = options.orderDir;
|
|
199
|
+
return makeRequest('list', args);
|
|
200
|
+
},
|
|
201
|
+
findFirst: async (options) => {
|
|
202
|
+
const args = { limit: 1 };
|
|
203
|
+
if (options?.where)
|
|
204
|
+
args.where = options.where;
|
|
205
|
+
const results = await makeRequest('list', args);
|
|
206
|
+
return Array.isArray(results) ? results[0] ?? null : null;
|
|
207
|
+
},
|
|
208
|
+
findUnique: async (options) => {
|
|
209
|
+
const args = { limit: 1 };
|
|
210
|
+
if (options?.where)
|
|
211
|
+
args.where = options.where;
|
|
212
|
+
const results = await makeRequest('list', args);
|
|
213
|
+
return Array.isArray(results) ? results[0] ?? null : null;
|
|
214
|
+
},
|
|
215
|
+
findById: async (id) => {
|
|
216
|
+
return makeRequest('get', { id });
|
|
217
|
+
},
|
|
218
|
+
count: async (options) => {
|
|
219
|
+
const args = {};
|
|
220
|
+
if (options?.where)
|
|
221
|
+
args.where = options.where;
|
|
222
|
+
const result = await makeRequest('count', args);
|
|
223
|
+
return result?.count ?? 0;
|
|
224
|
+
},
|
|
225
|
+
insert: async (data) => {
|
|
226
|
+
return makeMutation('create', { data });
|
|
227
|
+
},
|
|
228
|
+
insertMany: async (items) => {
|
|
229
|
+
const results = [];
|
|
230
|
+
for (const data of items) {
|
|
231
|
+
const result = await makeMutation('create', { data });
|
|
232
|
+
results.push(result);
|
|
233
|
+
}
|
|
234
|
+
return results;
|
|
235
|
+
},
|
|
236
|
+
create: async (options) => {
|
|
237
|
+
return makeMutation('create', { data: options.data });
|
|
238
|
+
},
|
|
239
|
+
update: async (options) => {
|
|
240
|
+
const id = options.where?.id;
|
|
241
|
+
if (!id)
|
|
242
|
+
throw new Error('Update requires an id in the where clause');
|
|
243
|
+
const result = await makeMutation('update', { id, data: options.data });
|
|
244
|
+
return result?.rowsAffected ?? 0;
|
|
245
|
+
},
|
|
246
|
+
upsert: async (options) => {
|
|
247
|
+
const id = options.where?.id;
|
|
248
|
+
const existing = id ? await makeRequest('get', { id }).catch(() => null) : null;
|
|
249
|
+
if (existing) {
|
|
250
|
+
await makeMutation('update', { id, data: options.update });
|
|
251
|
+
return { ...existing, ...options.update };
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
return makeMutation('create', { data: options.create });
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
delete: async (options) => {
|
|
258
|
+
const id = options.where?.id;
|
|
259
|
+
if (!id)
|
|
260
|
+
throw new Error('Delete requires an id in the where clause');
|
|
261
|
+
const result = await makeMutation('delete', { id });
|
|
262
|
+
return result?.rowsAffected ?? 0;
|
|
263
|
+
},
|
|
264
|
+
deleteById: async (id) => {
|
|
265
|
+
const result = await makeMutation('delete', { id });
|
|
266
|
+
return (result?.rowsAffected ?? 0) > 0;
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
72
272
|
function getWsUrl(config) {
|
|
73
273
|
const base = config.url
|
|
74
274
|
.replace('https://', 'wss://')
|
|
@@ -194,19 +394,54 @@ async function executeViaApi(config, functionName, functionType, args) {
|
|
|
194
394
|
async function handleCronTrigger(config, trigger) {
|
|
195
395
|
log.info(`Received trigger for ${trigger.functionName} (execution: ${trigger.executionId})`);
|
|
196
396
|
const startTime = Date.now();
|
|
197
|
-
const handler = cronHandlers.get(trigger.functionName);
|
|
198
397
|
try {
|
|
199
398
|
let result;
|
|
399
|
+
// 1. Check for manually registered handler
|
|
400
|
+
const handler = cronHandlers.get(trigger.functionName);
|
|
200
401
|
if (handler) {
|
|
201
|
-
|
|
202
|
-
log.debug(`Using local handler for: ${trigger.functionName}`);
|
|
402
|
+
log.debug(`Using registered handler for: ${trigger.functionName}`);
|
|
203
403
|
result = await handler(trigger.args);
|
|
204
404
|
}
|
|
205
405
|
else {
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
406
|
+
// 2. Try to load and execute from user's tether/functions
|
|
407
|
+
await loadFunctionRegistry();
|
|
408
|
+
const localFn = lookupFunction(trigger.functionName);
|
|
409
|
+
if (localFn) {
|
|
410
|
+
log.debug(`Executing local function: ${trigger.functionName}`);
|
|
411
|
+
// Create execution context with database proxy
|
|
412
|
+
const db = createDatabaseProxy(config.apiKey, config.url, config.projectId, config.environment);
|
|
413
|
+
// Create a simple logger for the function
|
|
414
|
+
const fnLog = {
|
|
415
|
+
log: (...args) => log.info(`[${trigger.functionName}]`, ...args),
|
|
416
|
+
debug: (...args) => log.debug(`[${trigger.functionName}]`, ...args),
|
|
417
|
+
info: (...args) => log.info(`[${trigger.functionName}]`, ...args),
|
|
418
|
+
warn: (...args) => log.warn(`[${trigger.functionName}]`, ...args),
|
|
419
|
+
error: (...args) => log.error(`[${trigger.functionName}]`, ...args),
|
|
420
|
+
};
|
|
421
|
+
// Create tether context with env vars from process.env
|
|
422
|
+
// Note: For cron execution, env vars should be set in the Nuxt app's environment
|
|
423
|
+
const tether = {
|
|
424
|
+
env: new Proxy({}, {
|
|
425
|
+
get(_target, key) {
|
|
426
|
+
return process.env[key];
|
|
427
|
+
},
|
|
428
|
+
}),
|
|
429
|
+
};
|
|
430
|
+
// Execute the function with context
|
|
431
|
+
result = await localFn.handler({
|
|
432
|
+
db,
|
|
433
|
+
args: trigger.args ?? {},
|
|
434
|
+
log: fnLog,
|
|
435
|
+
tether,
|
|
436
|
+
ctx: { auth: { userId: null, claims: {} }, userId: null },
|
|
437
|
+
auth: { getUserIdentity: async () => null },
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
// 3. Fall back to API for generic CRUD operations (table.operation)
|
|
442
|
+
log.debug(`Executing via API: ${trigger.functionName} (${trigger.functionType})`);
|
|
443
|
+
result = await executeViaApi(config, trigger.functionName, trigger.functionType, trigger.args);
|
|
444
|
+
}
|
|
210
445
|
}
|
|
211
446
|
const durationMs = Date.now() - startTime;
|
|
212
447
|
await reportExecution(config, trigger, {
|
|
@@ -336,14 +571,15 @@ export default defineNitroPlugin((nitro) => {
|
|
|
336
571
|
const apiKey = process.env.NUXT_TETHER_API_KEY || process.env.TETHER_API_KEY;
|
|
337
572
|
const url = process.env.NUXT_TETHER_URL || process.env.TETHER_URL || 'https://tether-api.strands.gg';
|
|
338
573
|
const projectId = process.env.NUXT_TETHER_PROJECT_ID || process.env.TETHER_PROJECT_ID;
|
|
574
|
+
const environment = process.env.NUXT_TETHER_ENVIRONMENT || process.env.TETHER_ENVIRONMENT;
|
|
339
575
|
verboseLogging = process.env.NUXT_TETHER_VERBOSE === 'true' || process.env.TETHER_VERBOSE === 'true';
|
|
340
576
|
// Only connect if we have all required config
|
|
341
577
|
if (!apiKey || !projectId) {
|
|
342
578
|
log.debug('Missing config (apiKey or projectId) - cron connection disabled');
|
|
343
579
|
return;
|
|
344
580
|
}
|
|
345
|
-
log.info(
|
|
346
|
-
const config = { url, projectId, apiKey };
|
|
581
|
+
log.info(`Initialising cron connection...${environment ? ` (env: ${environment})` : ''}`);
|
|
582
|
+
const config = { url, projectId, apiKey, environment };
|
|
347
583
|
// Connect on next tick to allow the server to fully initialise
|
|
348
584
|
process.nextTick(() => {
|
|
349
585
|
connect(config);
|
|
@@ -359,4 +595,4 @@ export default defineNitroPlugin((nitro) => {
|
|
|
359
595
|
log.debug('Connection closed');
|
|
360
596
|
});
|
|
361
597
|
});
|
|
362
|
-
//# sourceMappingURL=cron.js.map
|
|
598
|
+
//# sourceMappingURL=cron.js.map
|