mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-23 22:47:19 +00:00
feat(workers): expose app_uid in puter.workers.get and puter.workers.list (#3596)
This commit is contained in:
@@ -152,11 +152,11 @@ const waitFor = async (
|
||||
|
||||
const putCalls = () =>
|
||||
fetchSpy.mock.calls.filter(
|
||||
([, init]) => (init as RequestInit | undefined)?.method === 'PUT',
|
||||
(call) => (call[1] as RequestInit | undefined)?.method === 'PUT',
|
||||
);
|
||||
const deleteCalls = () =>
|
||||
fetchSpy.mock.calls.filter(
|
||||
([, init]) => (init as RequestInit | undefined)?.method === 'DELETE',
|
||||
(call) => (call[1] as RequestInit | undefined)?.method === 'DELETE',
|
||||
);
|
||||
|
||||
// -- create ----------------------------------------------------------
|
||||
@@ -504,6 +504,7 @@ describe('WorkerDriver.getFilePaths source resolution', () => {
|
||||
url: string;
|
||||
file_path: string | null;
|
||||
file_uid: string | null;
|
||||
app_uid: string | null;
|
||||
created_at: string | null;
|
||||
}>;
|
||||
|
||||
@@ -512,6 +513,7 @@ describe('WorkerDriver.getFilePaths source resolution', () => {
|
||||
expect(row.url).toBe(`https://${name}.puter.work`);
|
||||
expect(row.file_path).toBe(path);
|
||||
expect(row.file_uid).toBe(entry.uuid);
|
||||
expect(row.app_uid).toBeNull();
|
||||
expect(row.created_at).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
||||
});
|
||||
|
||||
@@ -619,7 +621,7 @@ describe('WorkerDriver hot reload', () => {
|
||||
);
|
||||
return !row;
|
||||
}, 'subdomain row removal after source delete');
|
||||
expect(deleteCalls().map(([u]) => u)).toContain(
|
||||
expect(deleteCalls().map((call) => call[0])).toContain(
|
||||
`${SCRIPTS_BASE}/${name}/`,
|
||||
);
|
||||
expect(path).toContain(user.username);
|
||||
@@ -646,7 +648,7 @@ describe('WorkerDriver hot reload', () => {
|
||||
);
|
||||
return !row;
|
||||
}, 'subdomain row removal after trash move');
|
||||
expect(deleteCalls().map(([u]) => u)).toContain(
|
||||
expect(deleteCalls().map((call) => call[0])).toContain(
|
||||
`${SCRIPTS_BASE}/${name}/`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -190,7 +190,7 @@ describe('WorkerDriver', () => {
|
||||
it('rejects reserved worker names', async () => {
|
||||
const serverWithReserved = await setupTestServer({
|
||||
reserved_words: ['admin', 'api'],
|
||||
});
|
||||
} as never);
|
||||
const driverWithReserved = serverWithReserved.drivers
|
||||
.workers as unknown as WorkerDriver;
|
||||
try {
|
||||
@@ -486,6 +486,46 @@ describe('WorkerDriver', () => {
|
||||
expect.arrayContaining([`${tag}-bound`, `${tag}-loose`]),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns app_uid for sandboxed/app-bound workers and null for user-scoped workers', async () => {
|
||||
const tag = `wkuid${Math.random().toString(36).slice(2, 8)}`;
|
||||
const owner = await makeUser(tag);
|
||||
const app = await server.stores.app.create(
|
||||
{
|
||||
name: `app-${tag}`,
|
||||
title: `app-${tag}`,
|
||||
index_url: `https://app-${tag}.example.com/`,
|
||||
},
|
||||
{ ownerUserId: owner.id },
|
||||
);
|
||||
await server.stores.subdomain.create({
|
||||
userId: owner.id,
|
||||
subdomain: `workers.puter.${tag}-bound`,
|
||||
rootDirId: null,
|
||||
associatedAppId: null,
|
||||
appOwner: app.id,
|
||||
});
|
||||
await server.stores.subdomain.create({
|
||||
userId: owner.id,
|
||||
subdomain: `workers.puter.${tag}-plain`,
|
||||
rootDirId: null,
|
||||
associatedAppId: null,
|
||||
appOwner: null,
|
||||
});
|
||||
|
||||
const seen = (await inCtx(
|
||||
() => target.getFilePaths({}),
|
||||
actorFor(owner),
|
||||
)) as Array<{ name: string; app_uid: string | null }>;
|
||||
|
||||
const bound = seen.find((r) => r.name === `${tag}-bound`);
|
||||
const plain = seen.find((r) => r.name === `${tag}-plain`);
|
||||
|
||||
expect(bound).toBeDefined();
|
||||
expect(bound?.app_uid).toBe(app.uid);
|
||||
expect(plain).toBeDefined();
|
||||
expect(plain?.app_uid).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFilePaths pagination', () => {
|
||||
@@ -586,7 +626,7 @@ describe('WorkerDriver', () => {
|
||||
it('returns the configured loggingUrl', async () => {
|
||||
const serverWithLogging = await setupTestServer({
|
||||
workers: { loggingUrl: 'https://logs.test/view' },
|
||||
});
|
||||
} as never);
|
||||
const driverWithLogging = serverWithLogging.drivers
|
||||
.workers as unknown as WorkerDriver;
|
||||
try {
|
||||
@@ -622,7 +662,7 @@ describe('WorkerDriver', () => {
|
||||
it('rejects an unverified account without the bypass', async () => {
|
||||
const strictServer = await setupTestServer({
|
||||
strict_email_verification_required: true,
|
||||
});
|
||||
} as never);
|
||||
const strictDriver = strictServer.drivers
|
||||
.workers as unknown as WorkerDriver;
|
||||
try {
|
||||
@@ -646,7 +686,7 @@ describe('WorkerDriver', () => {
|
||||
it('lets the bypass past the verified-email gate', async () => {
|
||||
const strictServer = await setupTestServer({
|
||||
strict_email_verification_required: true,
|
||||
});
|
||||
} as never);
|
||||
const strictDriver = strictServer.drivers
|
||||
.workers as unknown as WorkerDriver;
|
||||
try {
|
||||
@@ -725,7 +765,7 @@ describe('WorkerDriver', () => {
|
||||
it('cannot be forged through caller-supplied args', async () => {
|
||||
const strictServer = await setupTestServer({
|
||||
strict_email_verification_required: true,
|
||||
});
|
||||
} as never);
|
||||
const strictDriver = strictServer.drivers
|
||||
.workers as unknown as WorkerDriver;
|
||||
// Driver args reach the method as parsed JSON from the request
|
||||
|
||||
@@ -558,6 +558,11 @@ export class WorkerDriver extends PuterDriver {
|
||||
});
|
||||
}
|
||||
|
||||
const appOwnerIds = rows
|
||||
.map((r) => r.app_owner)
|
||||
.filter((id): id is number => typeof id === 'number');
|
||||
const appsById = await this.stores.app.getByIds(appOwnerIds);
|
||||
|
||||
const items = rows.map((r) => {
|
||||
const name =
|
||||
String(r.subdomain ?? '')
|
||||
@@ -570,11 +575,17 @@ export class WorkerDriver extends PuterDriver {
|
||||
file_path = loaded?.path;
|
||||
file_uid = loaded?.uuid;
|
||||
}
|
||||
let app_uid = null;
|
||||
if (typeof r.app_owner === 'number') {
|
||||
const loadedApp = appsById.get(r.app_owner);
|
||||
app_uid = loadedApp?.uid ?? null;
|
||||
}
|
||||
return {
|
||||
name,
|
||||
url: `https://${name}.puter.work`,
|
||||
file_path,
|
||||
file_uid,
|
||||
app_uid,
|
||||
created_at: r.ts
|
||||
? new Date(r.ts as string).toISOString()
|
||||
: null,
|
||||
|
||||
@@ -23,6 +23,10 @@ A string containing the file path of the worker source code.
|
||||
|
||||
A string containing the unique identifier of the worker file.
|
||||
|
||||
#### `app_uid` (String)
|
||||
|
||||
A string containing the unique identifier of the app or sandbox app associated with the worker, or `null` if the worker is user-scoped.
|
||||
|
||||
#### `created_at` (String)
|
||||
|
||||
A string containing the date and time when the worker was created.
|
||||
|
||||
@@ -11,6 +11,7 @@ import { fetchAllPages, iteratePages } from '../lib/pagination.js';
|
||||
* @property {string} url The URL of the worker.
|
||||
* @property {string} file_path The file path of the worker's source code.
|
||||
* @property {string} file_uid The unique identifier of the worker file.
|
||||
* @property {string | null} app_uid The unique identifier of the app or sandbox app owning the worker, or null if user-scoped.
|
||||
* @property {string} created_at The date and time when the worker was created.
|
||||
*/
|
||||
|
||||
|
||||
@@ -161,9 +161,20 @@ export default suite('workers', {
|
||||
},
|
||||
|
||||
'get returns the deployed worker': async (t) => {
|
||||
await deployWorker(t, 'workers-suite-get');
|
||||
const worker = await t.puter.workers.get('workers-suite-get');
|
||||
const name = 'workers-suite-get';
|
||||
await deployWorker(t, name);
|
||||
const worker = await t.puter.workers.get(name);
|
||||
t.assert.ok(worker, 'get should return the worker');
|
||||
const sandboxApp = await t.puter.apps.get(`sandbox-${name}`);
|
||||
t.assert.equal(worker.app_uid, sandboxApp.uid, 'worker app_uid should match sandbox app uid');
|
||||
},
|
||||
|
||||
'get returns null app_uid for unsandboxed worker': async (t) => {
|
||||
const name = 'workers-suite-get-plain';
|
||||
await deployWorker(t, name, { sandbox: false });
|
||||
const worker = await t.puter.workers.get(name);
|
||||
t.assert.ok(worker, 'get should return the worker');
|
||||
t.assert.equal(worker.app_uid, null, 'unsandboxed worker app_uid should be null');
|
||||
},
|
||||
|
||||
'list includes deployed workers': async (t) => {
|
||||
@@ -231,7 +242,7 @@ export default suite('workers', {
|
||||
|
||||
'create binds the worker to a named app': async (t) => {
|
||||
const appName = 'workers-suite-host-app';
|
||||
await t.puter.apps.create(appName, 'https://example.com/worker-host');
|
||||
const hostApp = await t.puter.apps.create(appName, 'https://example.com/worker-host');
|
||||
const name = 'workers-suite-bound';
|
||||
const sourcePath = `${home(t)}/workers-suite-${name}.js`;
|
||||
await t.puter.fs.write(sourcePath, WORKER_SOURCE);
|
||||
@@ -242,6 +253,8 @@ export default suite('workers', {
|
||||
false,
|
||||
'binding to an app should not create a sandbox app',
|
||||
);
|
||||
const worker = await t.puter.workers.get(name);
|
||||
t.assert.equal(worker?.app_uid, hostApp.uid, 'bound worker app_uid should match host app uid');
|
||||
},
|
||||
|
||||
'create with an unknown app name rejects with app_not_found': async (t) => {
|
||||
|
||||
Reference in New Issue
Block a user