@@ -3,6 +3,7 @@ const EventEmitter = require('node:events');
3
3
const ignore = require("ignore");
4
4
const tar = require("tar");
5
5
const fs = require("fs");
6
+ const ID = require("../id");
6
7
const childProcess = require('child_process');
7
8
const chokidar = require('chokidar');
8
9
const inquirer = require("inquirer");
@@ -11,6 +12,8 @@ const { Command } = require("commander");
11
12
const { localConfig, globalConfig } = require("../config");
12
13
const { paginate } = require('../paginate');
13
14
const { functionsListVariables } = require('./functions');
15
+ const { usersGet, usersCreateJWT } = require('./users');
16
+ const { projectsCreateJWT } = require('./projects');
14
17
const { questionsRunFunctions } = require("../questions");
15
18
const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser");
16
19
const { systemHasCommand, isPortTaken, getAllFiles } = require('../utils');
@@ -31,17 +34,67 @@ const systemTools = {
31
34
// TODO: Add all runtime needs
32
35
};
33
36
37
+ const JwtManager = {
38
+ userJwt: null,
39
+ functionJwt: null,
40
+
41
+ timerWarn: null,
42
+ timerError: null,
43
+
44
+ async setup(userId = null) {
45
+ if(this.timerWarn) {
46
+ clearTimeout(this.timerWarn);
47
+ }
48
+
49
+ if(this.timerError) {
50
+ clearTimeout(this.timerError);
51
+ }
52
+
53
+ this.timerWarn = setTimeout(() => {
54
+ log("Warning: Authorized JWT will expire in 5 minutes. Please stop and re-run the command to refresh tokens for 1 hour.");
55
+ }, 1000 * 60 * 55); // 55 mins
56
+
57
+ this.timerError = setTimeout(() => {
58
+ log("Warning: Authorized JWT just expired. Please stop and re-run the command to obtain new tokens with 1 hour validity.");
59
+ log("Some Appwrite API communication is not authorized now.")
60
+ }, 1000 * 60 * 60); // 60 mins
61
+
62
+ if(userId) {
63
+ await usersGet({
64
+ userId,
65
+ parseOutput: false
66
+ });
67
+ const userResponse = await usersCreateJWT({
68
+ userId,
69
+ duration: 60*60,
70
+ parseOutput: false
71
+ });
72
+ this.userJwt = userResponse.jwt;
73
+ }
74
+
75
+ const functionResponse = await projectsCreateJWT({
76
+ projectId: localConfig.getProject().projectId,
77
+ // TODO: There must be better way to get the list
78
+ scopes: ["sessions.write","users.read","users.write","teams.read","teams.write","databases.read","databases.write","collections.read","collections.write","attributes.read","attributes.write","indexes.read","indexes.write","documents.read","documents.write","files.read","files.write","buckets.read","buckets.write","functions.read","functions.write","execution.read","execution.write","locale.read","avatars.read","health.read","providers.read","providers.write","messages.read","messages.write","topics.read","topics.write","subscribers.read","subscribers.write","targets.read","targets.write","rules.read","rules.write","migrations.read","migrations.write","vcs.read","vcs.write","assistant.read"],
79
+ duration: 60*60,
80
+ parseOutput: false
81
+ });
82
+ this.functionJwt = functionResponse.jwt;
83
+ }
84
+ };
85
+
34
86
const Queue = {
35
87
files: [],
36
88
locked: false,
37
89
events: new EventEmitter(),
90
+ debounce: null,
38
91
push(file) {
39
92
if(!this.files.includes(file)) {
40
93
this.files.push(file);
41
94
}
42
95
43
96
if(!this.locked) {
44
- this.events.emit('reload', { files: this.files } );
97
+ this._trigger( );
45
98
}
46
99
},
47
100
lock() {
@@ -51,13 +104,23 @@ const Queue = {
51
104
unlock() {
52
105
this.locked = false;
53
106
if(this.files.length > 0) {
54
- this.events.emit('reload', { files: this.files } );
107
+ this._trigger( );
55
108
}
109
+ },
110
+ _trigger() {
111
+ if(this.debounce) {
112
+ return;
113
+ }
114
+
115
+ this.debounce = setTimeout(() => {
116
+ this.events.emit('reload', { files: this.files });
117
+ this.debounce = null;
118
+ }, 300);
56
119
}
57
120
};
58
121
59
122
async function dockerStop(id) {
60
- delete activeDockerIds[id];
123
+ delete activeDockerIds[id];
61
124
const stopProcess = childProcess.spawn('docker', ['rm', '--force', id], {
62
125
stdio: 'pipe',
63
126
});
@@ -92,7 +155,7 @@ async function dockerBuild(func, variables) {
92
155
93
156
const functionDir = path.join(process.cwd(), func.path);
94
157
95
- const id = `${new Date().getTime().toString(16)}${Math.round(Math.random() * 1000000000).toString(16)}` ;
158
+ const id = ID.unique() ;
96
159
97
160
const params = [ 'run' ];
98
161
params.push('--name', id);
@@ -102,8 +165,8 @@ async function dockerBuild(func, variables) {
102
165
params.push('-e', 'OPEN_RUNTIMES_SECRET=');
103
166
params.push('-e', `OPEN_RUNTIMES_ENTRYPOINT=${func.entrypoint}`);
104
167
105
- for(const v of variables) {
106
- params.push('-e', `${v.key }=${v.value }`);
168
+ for(const k of Object.keys( variables) ) {
169
+ params.push('-e', `${k }=${variables[k] }`);
107
170
}
108
171
109
172
params.push(imageName, 'sh', '-c', `helpers/build.sh "${func.commands}"`);
@@ -167,7 +230,7 @@ async function dockerStart(func, variables, port) {
167
230
168
231
const functionDir = path.join(process.cwd(), func.path);
169
232
170
- const id = `${new Date().getTime().toString(16)}${Math.round(Math.random() * 1000000000).toString(16)}` ;
233
+ const id = ID.unique() ;
171
234
172
235
const params = [ 'run' ];
173
236
params.push('--rm');
@@ -178,8 +241,8 @@ async function dockerStart(func, variables, port) {
178
241
params.push('-e', 'OPEN_RUNTIMES_ENV=development');
179
242
params.push('-e', 'OPEN_RUNTIMES_SECRET=');
180
243
181
- for(const v of variables) {
182
- params.push('-e', `${v.key }=${v.value }`);
244
+ for(const k of Object.keys( variables) ) {
245
+ params.push('-e', `${k }=${variables[k] }`);
183
246
}
184
247
185
248
params.push('-v', `${functionDir}/.appwrite/logs.txt:/mnt/logs/dev_logs.log:rw`);
@@ -215,7 +278,7 @@ async function dockerCleanup() {
215
278
}
216
279
}
217
280
218
- const runFunction = async ({ port, engine, functionId, noVariables, noReload } = {}) => {
281
+ const runFunction = async ({ port, engine, functionId, noVariables, noReload, userId } = {}) => {
219
282
// Selection
220
283
if(!functionId) {
221
284
const answers = await inquirer.prompt(questionsRunFunctions[0]);
@@ -265,14 +328,10 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload } =
265
328
}
266
329
267
330
if(engine === 'docker') {
268
- log('💡 Hint: Using system is faster, but using Docker simulates the production environment precisely.');
269
-
270
331
if(!systemHasCommand('docker')) {
271
332
return error("Please install Docker first: https://docs.docker.com/engine/install/");
272
333
}
273
334
} else if(engine === 'system') {
274
- log('💡 Hint: Docker simulates the production environment precisely, but using system is faster');
275
-
276
335
for(const command of tool.commands) {
277
336
if(!systemHasCommand(command.command)) {
278
337
return error(`Your system is missing command "${command.command}". Please install it first: ${command.docs}`);
@@ -317,25 +376,45 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload } =
317
376
fs.writeFileSync(errorsPath, '');
318
377
}
319
378
320
- let variables = [] ;
379
+ const variables = {} ;
321
380
if(!noVariables) {
322
381
if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') {
323
382
error("No user is signed in. To sign in, run: appwrite login. Function will run locally, but will not have your function's environment variables set.");
324
383
} else {
325
- const { variables: remoteVariables } = await paginate(functionsListVariables, {
326
- functionId: func['$id'],
327
- parseOutput: false
328
- }, 100, 'variables');
329
-
330
- remoteVariables.forEach((v) => {
331
- variables.push({
332
- key: v.key,
333
- value: v.value
384
+ try {
385
+ const { variables: remoteVariables } = await paginate(functionsListVariables, {
386
+ functionId: func['$id'],
387
+ parseOutput: false
388
+ }, 100, 'variables');
389
+
390
+ remoteVariables.forEach((v) => {
391
+ variables[v.key] = v.value;
334
392
});
335
- });
393
+ } catch(err) {
394
+ error("Could not fetch remote variables: " + err.message);
395
+ error("Function will run locally, but will not have your function's environment variables set.");
396
+ }
336
397
}
337
398
}
338
399
400
+ variables['APPWRITE_FUNCTION_API_ENDPOINT'] = globalConfig.getFrom('endpoint');
401
+ variables['APPWRITE_FUNCTION_ID'] = func.$id;
402
+ variables['APPWRITE_FUNCTION_NAME'] = func.name;
403
+ variables['APPWRITE_FUNCTION_DEPLOYMENT'] = ''; // TODO: Implement when relevant
404
+ variables['APPWRITE_FUNCTION_PROJECT_ID'] = localConfig.getProject().projectId;
405
+ variables['APPWRITE_FUNCTION_RUNTIME_NAME'] = ''; // TODO: Implement when relevant
406
+ variables['APPWRITE_FUNCTION_RUNTIME_VERSION'] = ''; // TODO: Implement when relevant
407
+
408
+ await JwtManager.setup(userId);
409
+
410
+ const headers = {};
411
+ headers['x-appwrite-key'] = JwtManager.functionJwt ?? '';
412
+ headers['x-appwrite-trigger'] = 'http';
413
+ headers['x-appwrite-event'] = '';
414
+ headers['x-appwrite-user-id'] = userId ?? '';
415
+ headers['x-appwrite-user-jwt'] = JwtManager.userJwt ?? '';
416
+ variables['OPEN_RUNTIMES_HEADERS'] = JSON.stringify(headers);
417
+
339
418
await dockerPull(func);
340
419
await dockerBuild(func, variables);
341
420
await dockerStart(func, variables, port);
@@ -348,6 +427,7 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload } =
348
427
});
349
428
350
429
if(!noReload) {
430
+ // TODO: Stop previous job mid-way if new deployment is ready, I think?
351
431
chokidar.watch('.', {
352
432
cwd: path.join(process.cwd(), func.path),
353
433
ignoreInitial: true,
460
540
.option(`--functionId <functionId >`, `Function ID`)
461
541
.option(`--port <port >`, `Local port`)
462
542
.option(`--engine <engine >`, `Local engine, "system" or "docker"`)
543
+ .option(`--userId <userId >`, `ID of user to impersonate`)
463
544
.option(`--noVariables`, `Prevent pulling variables from function settings`)
464
545
.option(`--noReload`, `Prevent live reloading of server when changes are made to function files`)
465
546
.action(actionRunner(runFunction));
0 commit comments