@thatopen/services 0.16.0 → 0.16.1
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/docs/ai-quickstart.md +15 -5
- package/docs/platform-token.md +1 -1
- package/package.json +2 -1
- package/src/cli/templates/app/src/main.ts +27 -11
- package/src/core/examples/.env.example +7 -0
- package/src/core/examples/apps.ts +67 -0
- package/src/core/examples/batch-reads.ts +99 -0
- package/src/core/examples/channel.ts +65 -0
- package/src/core/examples/components.ts +91 -0
- package/src/core/examples/execution.ts +75 -0
- package/src/core/examples/file-metadata.ts +94 -0
- package/src/core/examples/files.ts +81 -0
- package/src/core/examples/folders.ts +82 -0
- package/src/core/examples/hidden-files.ts +81 -0
- package/src/core/examples/icons.ts +68 -0
- package/src/core/examples/permissions.ts +87 -0
package/docs/ai-quickstart.md
CHANGED
|
@@ -51,7 +51,8 @@ Steps 0–2 are identical for both; they diverge only at `create` (step 3) and h
|
|
|
51
51
|
|
|
52
52
|
- **Node.js ≥ 18** and **npm** — verify with `node -v`.
|
|
53
53
|
- A **That Open Platform API token**. The user creates it themselves:
|
|
54
|
-
→ **https://platform.thatopen.com/dashboard
|
|
54
|
+
→ **https://platform.thatopen.com/dashboard → Account → Tokens → create → copy.**
|
|
55
|
+
(The Data overview shows the same Tokens card; both paths work.)
|
|
55
56
|
Ask the user to paste it. **Never print it back, never write it into a file, never commit it.**
|
|
56
57
|
|
|
57
58
|
## 1. Install the CLI
|
|
@@ -62,8 +63,9 @@ npm install -g @thatopen/services@latest
|
|
|
62
63
|
|
|
63
64
|
## 2. Log in (do this BEFORE creating the project)
|
|
64
65
|
|
|
65
|
-
Get the token from the platform dashboard — **https://platform.thatopen.com/dashboard
|
|
66
|
-
→ **
|
|
66
|
+
Get the token from the platform dashboard — **https://platform.thatopen.com/dashboard**
|
|
67
|
+
→ **Account → Tokens** → create → copy (the Data overview shows the same card). Ask the
|
|
68
|
+
user to paste it, then run:
|
|
67
69
|
|
|
68
70
|
```bash
|
|
69
71
|
thatopen login --token <platform-token>
|
|
@@ -304,8 +306,16 @@ socket.on("channelMessage", (msg) => {
|
|
|
304
306
|
```
|
|
305
307
|
|
|
306
308
|
A reply to your `requestId` reaches only your socket — a second tool connected
|
|
307
|
-
at the same time never sees it.
|
|
308
|
-
|
|
309
|
+
at the same time never sees it. Replies arrive as a `channelMessage` with
|
|
310
|
+
`type: "reply"` and your `requestId` — match on the `requestId`, not the type.
|
|
311
|
+
|
|
312
|
+
`{ delivered: 0 }` on the ack means no tab of that account's app has the
|
|
313
|
+
channel joined — usually the app is simply not open. Apps scaffolded before
|
|
314
|
+
services 0.16.1 had one more way here: they joined the channel only at the END
|
|
315
|
+
of boot, so a stalled startup request left an app that looked healthy but had
|
|
316
|
+
never joined, with nothing in the console either way (0.16.1 scaffolds join
|
|
317
|
+
before anything boot can stall on). With the app visibly open and still
|
|
318
|
+
`delivered: 0`, **ask the user to reload the app tab and retry**.
|
|
309
319
|
|
|
310
320
|
To grow the app's command surface, extend `AppCommands` in
|
|
311
321
|
`src/setups/channel.ts` and handle the new command — the handler's return value
|
package/docs/platform-token.md
CHANGED
|
@@ -8,7 +8,7 @@ here rather than in each of them.
|
|
|
8
8
|
Say this to the user, and wait:
|
|
9
9
|
|
|
10
10
|
> "To log in I need a That Open Platform **access token**. You create it yourself:
|
|
11
|
-
> **https://platform.thatopen.com/dashboard
|
|
11
|
+
> **https://platform.thatopen.com/dashboard → Account → Tokens → create → copy** (the Data overview shows the same card), then paste it
|
|
12
12
|
> here."
|
|
13
13
|
|
|
14
14
|
**Never print it back, never write it into a file, never commit it.** Not into a script, not into a
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
17
|
"private": false,
|
|
18
|
-
"version": "0.16.
|
|
18
|
+
"version": "0.16.1",
|
|
19
19
|
"main": "dist/index.cjs.js",
|
|
20
20
|
"module": "dist/index.es.js",
|
|
21
21
|
"types": "dist/index.d.ts",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|
|
27
27
|
"src/cli/templates",
|
|
28
|
+
"src/core/examples",
|
|
28
29
|
"src/built-in",
|
|
29
30
|
"docs",
|
|
30
31
|
"resources"
|
|
@@ -40,6 +40,20 @@ async function main() {
|
|
|
40
40
|
);
|
|
41
41
|
components.get(UIManager).init();
|
|
42
42
|
|
|
43
|
+
// The platform channel, pre-wired (see ./setups/channel.ts): external tools
|
|
44
|
+
// of this account (an MCP server, a CLI, a plugin) can command this app, and
|
|
45
|
+
// other open tabs of it are reachable for collaboration. Joined HERE, before
|
|
46
|
+
// any await the boot can park on (the world, project data): a stalled fetch
|
|
47
|
+
// further down must not leave the app unreachable — and silently so, since a
|
|
48
|
+
// join that is never requested also never gets to time out loudly. It only
|
|
49
|
+
// exists inside the platform iframe, so a failure is a warning, never a boot
|
|
50
|
+
// error.
|
|
51
|
+
try {
|
|
52
|
+
setupChannel(client, components);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn("[channel] unavailable outside the platform:", error);
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
// One STABLE top-viewer node, returned by reference so re-rendering top-app
|
|
44
58
|
// (when we add the panels below) reuses it instead of disposing/recreating
|
|
45
59
|
// its world. No <top-viewer-tools>: the bim-viewer mounts its own tabbed
|
|
@@ -97,22 +111,24 @@ async function main() {
|
|
|
97
111
|
const projectId: string | undefined = client?.context?.projectId;
|
|
98
112
|
let projectData;
|
|
99
113
|
try {
|
|
100
|
-
|
|
114
|
+
// Bounded: this request carries no AbortSignal, so a stalled response
|
|
115
|
+
// parked everything below it for good (that is how the app once ended up
|
|
116
|
+
// booted but with no panels and, before the channel moved up top, deaf to
|
|
117
|
+
// external tools). Past the deadline the app boots without project data —
|
|
118
|
+
// the same degraded path the catch below already provides.
|
|
119
|
+
if (projectId) {
|
|
120
|
+
projectData = await Promise.race([
|
|
121
|
+
client.getProjectData(projectId),
|
|
122
|
+
new Promise<undefined>((resolve) => {
|
|
123
|
+
setTimeout(() => resolve(undefined), 20_000);
|
|
124
|
+
}),
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
101
127
|
} catch {
|
|
102
128
|
/* dev/no-project → consumers degrade gracefully */
|
|
103
129
|
}
|
|
104
130
|
setAppContext(client, projectData);
|
|
105
131
|
|
|
106
|
-
// The platform channel, pre-wired (see ./setups/channel.ts): external tools
|
|
107
|
-
// of this account (an MCP server, a CLI, a plugin) can command this app, and
|
|
108
|
-
// other open tabs of it are reachable for collaboration. It only exists
|
|
109
|
-
// inside the platform iframe, so a failure is a warning, never a boot error.
|
|
110
|
-
try {
|
|
111
|
-
setupChannel(client, components);
|
|
112
|
-
} catch (error) {
|
|
113
|
-
console.warn("[channel] unavailable outside the platform:", error);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
132
|
// Pluggable loaders for <top-models-list>. The built-in ships the lightweight
|
|
117
133
|
// defaults (.frag load, IFC→fragments convert); heavy/app-specific loaders are
|
|
118
134
|
// registered here so they stay OUT of the built-in's bundle. The reality-capture
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ACCESS_TOKEN=your-access-token-here
|
|
2
|
+
API_URL=https://api.thatopen.com
|
|
3
|
+
PROJECT_ID=your-project-id-here
|
|
4
|
+
# Required only for permissions.ts — obtain from DevTools → Network → any /api request → Authorization header (without "Bearer ")
|
|
5
|
+
JWT=
|
|
6
|
+
# Required only for execution.ts — obtain from listComponents() or the platform dashboard
|
|
7
|
+
COMPONENT_ID=
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// description: "App lifecycle — list, get with versions, create, download bundle, and archive."
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { EngineServicesClient } from '../client';
|
|
5
|
+
|
|
6
|
+
config({ path: resolve(__dirname, '.env') });
|
|
7
|
+
|
|
8
|
+
const ACCESS_TOKEN = process.env.ACCESS_TOKEN;
|
|
9
|
+
const API_URL = process.env.API_URL;
|
|
10
|
+
const PROJECT_ID = process.env.PROJECT_ID;
|
|
11
|
+
|
|
12
|
+
if (!ACCESS_TOKEN || !API_URL) {
|
|
13
|
+
throw new Error('ACCESS_TOKEN and API_URL are required in src/core/examples/.env');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const client = new EngineServicesClient(ACCESS_TOKEN!, API_URL!);
|
|
18
|
+
|
|
19
|
+
// --- List ---
|
|
20
|
+
const apps = await client.listApps();
|
|
21
|
+
console.log(`\nFound ${apps.length} app(s):`);
|
|
22
|
+
for (const app of apps) {
|
|
23
|
+
console.log(` [${app._id}] ${app.name}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Get ---
|
|
27
|
+
if (apps.length > 0) {
|
|
28
|
+
const detail = await client.listApps({ ShowVersions: true });
|
|
29
|
+
console.log('\nFirst app detail (with versions):');
|
|
30
|
+
console.log(JSON.stringify(detail[0], null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!PROJECT_ID) {
|
|
34
|
+
console.log('\nSkipping create/download/archive — set PROJECT_ID in .env to run the full cycle.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Create ---
|
|
39
|
+
// The file must be a valid ZIP produced by the thatopen CLI toolchain (`thatopen publish`).
|
|
40
|
+
// Uploading an invalid or empty ZIP causes the server to close the connection — use a real built app ZIP here.
|
|
41
|
+
// appProps is optional; omitting it uses platform defaults.
|
|
42
|
+
const bundle = new Blob([''], { type: 'application/zip' });
|
|
43
|
+
const appName = `__example-test__-${Date.now()}`;
|
|
44
|
+
const created = await client.createApp({
|
|
45
|
+
file: bundle,
|
|
46
|
+
name: appName,
|
|
47
|
+
versionTag: 'v1',
|
|
48
|
+
projectId: PROJECT_ID,
|
|
49
|
+
});
|
|
50
|
+
console.log(`\nCreated app: [${created.item._id}] ${created.item.name}`);
|
|
51
|
+
|
|
52
|
+
const appId = created.item._id;
|
|
53
|
+
|
|
54
|
+
// --- Download bundle ---
|
|
55
|
+
// downloadAppBundle extracts only the JS bundle from the ZIP — not the full archive.
|
|
56
|
+
const bundleResponse = await client.downloadAppBundle(appId);
|
|
57
|
+
const bundleText = await bundleResponse.text();
|
|
58
|
+
console.log(`\nDownloaded bundle (latest): ${bundleText.slice(0, 80)}...`);
|
|
59
|
+
|
|
60
|
+
// --- Archive ---
|
|
61
|
+
// archiveApp is a soft-delete. Unlike files and components, there is no recoverApp method —
|
|
62
|
+
// archived apps can only be managed via the platform UI.
|
|
63
|
+
await client.archiveApp(appId);
|
|
64
|
+
console.log(`\nArchived app [${appId}] — account is clean.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// description: "Batch reads — sign many hidden files, list versions and metadata for many files, and resolve many folders in one request each."
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { EngineServicesClient } from '../client';
|
|
5
|
+
|
|
6
|
+
config({ path: resolve(__dirname, '.env') });
|
|
7
|
+
|
|
8
|
+
const ACCESS_TOKEN = process.env.ACCESS_TOKEN;
|
|
9
|
+
const API_URL = process.env.API_URL;
|
|
10
|
+
const PROJECT_ID = process.env.PROJECT_ID;
|
|
11
|
+
|
|
12
|
+
if (!ACCESS_TOKEN || !API_URL) {
|
|
13
|
+
throw new Error('ACCESS_TOKEN and API_URL are required in src/core/examples/.env');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const client = new EngineServicesClient(ACCESS_TOKEN!, API_URL!);
|
|
18
|
+
|
|
19
|
+
if (!PROJECT_ID) {
|
|
20
|
+
console.log('Set PROJECT_ID in .env to run this example.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const files = await client.listFiles({ projectId: PROJECT_ID });
|
|
25
|
+
if (!files.length) {
|
|
26
|
+
console.log('No files in this project — nothing to batch.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const fileIds = files.map((file) => String(file._id));
|
|
30
|
+
|
|
31
|
+
// --- Versions for a whole page of files ---
|
|
32
|
+
// One request instead of one per file. Entries come back in request order,
|
|
33
|
+
// and a file the token cannot read carries an `error` instead of `versions`.
|
|
34
|
+
const versionEntries = await client.listVersionsBatch(fileIds);
|
|
35
|
+
console.log('\nVersions per file:');
|
|
36
|
+
for (const entry of versionEntries) {
|
|
37
|
+
if (entry.error) {
|
|
38
|
+
console.log(` ${entry.itemId} → ${entry.error.status} ${entry.error.message}`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
console.log(` ${entry.itemId} → ${entry.versions?.length ?? 0} version(s)`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Metadata for specific (file, version) pairs ---
|
|
45
|
+
// The versions above already carry their metadata, so this is only needed
|
|
46
|
+
// when the version list itself is not wanted.
|
|
47
|
+
const pairs = versionEntries
|
|
48
|
+
.filter((entry) => entry.versions?.length)
|
|
49
|
+
.map((entry) => ({
|
|
50
|
+
itemId: entry.itemId,
|
|
51
|
+
versionTag: entry.versions![0].tag,
|
|
52
|
+
}));
|
|
53
|
+
const metadataEntries = await client.getFileVersionMetadataBatch(pairs);
|
|
54
|
+
console.log('\nLatest-version metadata:');
|
|
55
|
+
for (const entry of metadataEntries) {
|
|
56
|
+
console.log(
|
|
57
|
+
` ${entry.itemId}@${entry.versionTag} → ${JSON.stringify(entry.metadata ?? entry.error)}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Folders by id ---
|
|
62
|
+
// Use listFolders({ projectId }) for the whole tree; this is for a known set
|
|
63
|
+
// of ids, such as the parents behind a breadcrumb.
|
|
64
|
+
const folderIds = [
|
|
65
|
+
...new Set(files.map((file) => file.folderId).filter(Boolean).map(String)),
|
|
66
|
+
];
|
|
67
|
+
if (folderIds.length) {
|
|
68
|
+
const folderEntries = await client.getFoldersBatch(folderIds);
|
|
69
|
+
console.log('\nFolders:');
|
|
70
|
+
for (const entry of folderEntries) {
|
|
71
|
+
console.log(` ${entry.folderId} → ${entry.folder?.name ?? entry.error?.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Signed URLs for many hidden files ---
|
|
76
|
+
// This is the call tile-based viewers (splats, point clouds) should use.
|
|
77
|
+
// Minting one URL per tile as the camera moves is what pushes a single
|
|
78
|
+
// session past the rate limit; one request covers up to 100 tiles, and
|
|
79
|
+
// longer lists are split into several requests automatically.
|
|
80
|
+
const hiddenFiles = await client.getHiddenFilesByParent(fileIds[0]);
|
|
81
|
+
if (!hiddenFiles.length) {
|
|
82
|
+
console.log('\nNo hidden files on the first file — skipping signed URLs.');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const signedEntries = await client.getHiddenFileSignedUrlsBatch(
|
|
86
|
+
hiddenFiles.map((hiddenFile) => String(hiddenFile._id)),
|
|
87
|
+
3600,
|
|
88
|
+
);
|
|
89
|
+
console.log('\nSigned hidden file URLs:');
|
|
90
|
+
for (const entry of signedEntries) {
|
|
91
|
+
if (entry.error) {
|
|
92
|
+
console.log(` ${entry.hiddenFileId} → ${entry.error.status} ${entry.error.message}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
console.log(` ${entry.hiddenFileId} → expires ${entry.expiresAt}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// description: "Platform channel (app-side, runs inside the platform iframe) — a collaboration room with presence, and typed commands from external tools (MCP, CLI, Revit, etc) with automatic replies."
|
|
2
|
+
import { PlatformClient } from '../platform-client';
|
|
3
|
+
|
|
4
|
+
// Unlike the other examples this is not a Node script: `client.channel` only
|
|
5
|
+
// exists inside an app running in the platform's iframe, and throws anywhere
|
|
6
|
+
// else. Copy `setupChannel` into your app.
|
|
7
|
+
|
|
8
|
+
// The contract with an external tool, written once. Each command names its
|
|
9
|
+
// payload and, if the caller waits for an answer, its reply. Use `type`, and
|
|
10
|
+
// list only what the app handles.
|
|
11
|
+
type McpCommands = {
|
|
12
|
+
'select-by-category': {
|
|
13
|
+
payload: { category: string };
|
|
14
|
+
reply: { count: number };
|
|
15
|
+
};
|
|
16
|
+
'get-loaded-models': { payload: undefined; reply: { models: string[] } };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// What tabs of this app send each other. No replies: these are events.
|
|
20
|
+
type CollabEvents = {
|
|
21
|
+
'cursor-position': { userName: string; x: number; y: number; z: number };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function setupChannel(client: PlatformClient) {
|
|
25
|
+
// Commands from this account's MCP server only. `external()` with no kind
|
|
26
|
+
// hears every kind of external tool (MCP, CLI, Revit plugin, etc...).
|
|
27
|
+
const mcp = client.channel.external<McpCommands>('mcp');
|
|
28
|
+
|
|
29
|
+
// What you return is sent back to whoever asked. Throw and it gets an error
|
|
30
|
+
// instead of waiting for a timeout. A command that was sent without waiting
|
|
31
|
+
// for an answer (a notification) has its return value ignored.
|
|
32
|
+
mcp.on('select-by-category', async ({ category }) => {
|
|
33
|
+
const count = await selectByCategory(category);
|
|
34
|
+
return { count };
|
|
35
|
+
});
|
|
36
|
+
mcp.on('get-loaded-models', () => ({ models: listModels() }));
|
|
37
|
+
|
|
38
|
+
// Joining is opt-in. Nothing reaches this app until it joins, so wire it to
|
|
39
|
+
// a button, or join at startup if the app should always listen.
|
|
40
|
+
await mcp.join();
|
|
41
|
+
|
|
42
|
+
// Collaboration: other users' tabs of this same app, across accounts.
|
|
43
|
+
const collab = client.channel.collab<CollabEvents>();
|
|
44
|
+
collab.on('cursor-position', (cursor, { from }) => moveAvatar(from, cursor));
|
|
45
|
+
// `from` identifies a tab, so a departure removes exactly its avatar.
|
|
46
|
+
collab.onPeerLeft(({ from }) => removeAvatar(from));
|
|
47
|
+
await collab.join();
|
|
48
|
+
|
|
49
|
+
// Sending does nothing until the platform has confirmed the join.
|
|
50
|
+
collab.send('cursor-position', { userName: 'Ada', x: 0, y: 0, z: 0 });
|
|
51
|
+
|
|
52
|
+
// Being in a room says this tab is listening, not that anyone is there.
|
|
53
|
+
// `peers` is who the platform reports on the other end right now.
|
|
54
|
+
mcp.onChange(() => {
|
|
55
|
+
console.log(`MCP: ${mcp.joined ? 'listening' : 'off'}, ${mcp.peers.length} connected`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return { mcp, collab };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Stand-ins for the app's own logic.
|
|
62
|
+
declare function selectByCategory(category: string): Promise<number>;
|
|
63
|
+
declare function listModels(): string[];
|
|
64
|
+
declare function moveAvatar(id: string, cursor: unknown): void;
|
|
65
|
+
declare function removeAvatar(id: string): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// description: "Cloud component lifecycle — list, get with versions, create, update, download bundle, archive, and recover."
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { EngineServicesClient } from '../client';
|
|
5
|
+
|
|
6
|
+
config({ path: resolve(__dirname, '.env') });
|
|
7
|
+
|
|
8
|
+
const ACCESS_TOKEN = process.env.ACCESS_TOKEN;
|
|
9
|
+
const API_URL = process.env.API_URL;
|
|
10
|
+
const PROJECT_ID = process.env.PROJECT_ID;
|
|
11
|
+
|
|
12
|
+
if (!ACCESS_TOKEN || !API_URL) {
|
|
13
|
+
throw new Error('ACCESS_TOKEN and API_URL are required in src/core/examples/.env');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const client = new EngineServicesClient(ACCESS_TOKEN!, API_URL!);
|
|
18
|
+
|
|
19
|
+
// --- List ---
|
|
20
|
+
const components = await client.listComponents();
|
|
21
|
+
console.log(`\nFound ${components.length} component(s):`);
|
|
22
|
+
for (const c of components) {
|
|
23
|
+
console.log(` [${c._id}] ${c.name}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Get ---
|
|
27
|
+
if (components.length > 0) {
|
|
28
|
+
const detail = await client.getComponent(components[0]._id, { showVersions: true });
|
|
29
|
+
console.log('\nFirst component detail (with versions):');
|
|
30
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!PROJECT_ID) {
|
|
34
|
+
console.log('\nSkipping create/update/download/archive — set PROJECT_ID in .env to run the full cycle.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Create ---
|
|
39
|
+
// componentProps is required for TOOL items — the backend rejects creation without it.
|
|
40
|
+
// The file must be a valid ZIP produced by the thatopen CLI toolchain (`thatopen publish`).
|
|
41
|
+
// Uploading a raw JS blob returns 422 — use a real built component ZIP here.
|
|
42
|
+
const bundle = new Blob(['export async function main() { return { type: "SUCCESS", message: "ok" }; }'], { type: 'application/javascript' });
|
|
43
|
+
const componentName = `__example-test__-${Date.now()}`;
|
|
44
|
+
const created = await client.createComponent({
|
|
45
|
+
file: bundle,
|
|
46
|
+
name: componentName,
|
|
47
|
+
versionTag: 'v1',
|
|
48
|
+
projectId: PROJECT_ID,
|
|
49
|
+
componentProps: { type: 'CLOUD', tier: 'FREE', isPublic: false },
|
|
50
|
+
});
|
|
51
|
+
console.log(`\nCreated component: [${created.item._id}] ${created.item.name}`);
|
|
52
|
+
|
|
53
|
+
const componentId = created.item._id;
|
|
54
|
+
|
|
55
|
+
// --- Update (rename) ---
|
|
56
|
+
const renamed = await client.updateComponent(componentId, { name: `${componentName}-renamed` });
|
|
57
|
+
console.log(`\nRenamed to: ${renamed.item?.name}`);
|
|
58
|
+
|
|
59
|
+
// --- Update (new version) ---
|
|
60
|
+
const bundleV2 = new Blob(['export async function main() { return { type: "SUCCESS", message: "v2" }; }'], { type: 'application/javascript' });
|
|
61
|
+
const versioned = await client.updateComponent(componentId, {
|
|
62
|
+
file: bundleV2,
|
|
63
|
+
versionTag: 'v2',
|
|
64
|
+
componentProps: { type: 'CLOUD', tier: 'FREE', isPublic: false },
|
|
65
|
+
});
|
|
66
|
+
console.log(`\nUploaded new version: ${versioned.version?.tag}`);
|
|
67
|
+
|
|
68
|
+
// --- Download bundle ---
|
|
69
|
+
// downloadComponentBundle extracts only the JS bundle from the ZIP — not the full archive.
|
|
70
|
+
const bundleResponse = await client.downloadComponentBundle(componentId);
|
|
71
|
+
const bundleText = await bundleResponse.text();
|
|
72
|
+
console.log(`\nDownloaded bundle (latest): ${bundleText.slice(0, 80)}...`);
|
|
73
|
+
|
|
74
|
+
const bundleV1Response = await client.downloadComponentBundle(componentId, { versionTag: 'v1' });
|
|
75
|
+
const bundleV1Text = await bundleV1Response.text();
|
|
76
|
+
console.log(`Downloaded bundle (v1): ${bundleV1Text.slice(0, 80)}...`);
|
|
77
|
+
|
|
78
|
+
// --- Archive / Recover ---
|
|
79
|
+
// archiveComponent is a soft-delete — recoverable with recoverComponent.
|
|
80
|
+
await client.archiveComponent(componentId);
|
|
81
|
+
console.log(`\nArchived component [${componentId}]`);
|
|
82
|
+
|
|
83
|
+
await client.recoverComponent(componentId);
|
|
84
|
+
console.log('Recovered component.');
|
|
85
|
+
|
|
86
|
+
// Archive again to clean up.
|
|
87
|
+
await client.archiveComponent(componentId);
|
|
88
|
+
console.log(`Re-archived component — account is clean.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// description: "Cloud component execution — trigger, track progress via WebSocket, list, get, and abort executions."
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { EngineServicesClient } from '../client';
|
|
5
|
+
|
|
6
|
+
config({ path: resolve(__dirname, '.env') });
|
|
7
|
+
|
|
8
|
+
const ACCESS_TOKEN = process.env.ACCESS_TOKEN;
|
|
9
|
+
const API_URL = process.env.API_URL;
|
|
10
|
+
const PROJECT_ID = process.env.PROJECT_ID;
|
|
11
|
+
|
|
12
|
+
// Required: a published cloud component ID.
|
|
13
|
+
// Obtain it from `listComponents()` or from the platform dashboard.
|
|
14
|
+
// Set it as COMPONENT_ID in src/core/examples/.env.
|
|
15
|
+
const COMPONENT_ID = process.env.COMPONENT_ID;
|
|
16
|
+
|
|
17
|
+
if (!ACCESS_TOKEN || !API_URL) {
|
|
18
|
+
throw new Error('ACCESS_TOKEN and API_URL are required in src/core/examples/.env');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!COMPONENT_ID) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'COMPONENT_ID is required to run execution.ts.\n' +
|
|
24
|
+
'Get it from `npx tsx src/core/examples/components.ts` once you have a published component,\n' +
|
|
25
|
+
'or from the platform dashboard. Set it as COMPONENT_ID in src/core/examples/.env.',
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const client = new EngineServicesClient(ACCESS_TOKEN!, API_URL!);
|
|
31
|
+
|
|
32
|
+
// --- Execute ---
|
|
33
|
+
// Pass projectId in executionParams to scope the execution to a project.
|
|
34
|
+
// The backend validates that the component is linked to that project — a foreign projectId returns 403.
|
|
35
|
+
// Omit projectId for personal (ownership) executions.
|
|
36
|
+
const { executionId } = await client.executeComponent(
|
|
37
|
+
COMPONENT_ID!,
|
|
38
|
+
{ ...(PROJECT_ID && { projectId: PROJECT_ID }) },
|
|
39
|
+
);
|
|
40
|
+
console.log(`\nStarted execution: [${executionId}]`);
|
|
41
|
+
|
|
42
|
+
// --- Track progress via WebSocket ---
|
|
43
|
+
// onExecutionProgress fires on each progress event until the execution completes.
|
|
44
|
+
// The socket is not automatically closed — the callback receives a final event when done.
|
|
45
|
+
await new Promise<void>((resolve) => {
|
|
46
|
+
client.onExecutionProgress(executionId, (data) => {
|
|
47
|
+
if (data.progressUpdate) {
|
|
48
|
+
console.log(` progress: ${data.progressUpdate.progress}%`);
|
|
49
|
+
if (data.progressUpdate.result) {
|
|
50
|
+
console.log(` result: ${data.progressUpdate.result}`, data.progressUpdate.resultMessage ?? '');
|
|
51
|
+
resolve();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (data.messageUpdate) {
|
|
55
|
+
console.log(` message: ${data.messageUpdate.content}`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// --- Get execution detail ---
|
|
61
|
+
const execution = await client.getExecution(executionId);
|
|
62
|
+
console.log('\nExecution detail:');
|
|
63
|
+
console.log(JSON.stringify(execution, null, 2));
|
|
64
|
+
|
|
65
|
+
// --- List executions for the component ---
|
|
66
|
+
const executions = await client.listExecutions(COMPONENT_ID!);
|
|
67
|
+
console.log(`\nTotal executions for component: ${executions.length}`);
|
|
68
|
+
|
|
69
|
+
// --- Abort (demonstrates the API — only useful on long-running executions) ---
|
|
70
|
+
// abortExecution on an already-completed execution is a no-op on most backends.
|
|
71
|
+
// In production, call it before the WebSocket resolves to cancel a running execution.
|
|
72
|
+
console.log('\n(Skipping abortExecution — execution already completed.)');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// description: "File metadata and versioning — get, update, and delete per-version metadata; list, archive, recover, and permanently delete versions."
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { EngineServicesClient } from '../client';
|
|
5
|
+
|
|
6
|
+
config({ path: resolve(__dirname, '.env') });
|
|
7
|
+
|
|
8
|
+
const ACCESS_TOKEN = process.env.ACCESS_TOKEN;
|
|
9
|
+
const API_URL = process.env.API_URL;
|
|
10
|
+
const PROJECT_ID = process.env.PROJECT_ID;
|
|
11
|
+
|
|
12
|
+
if (!ACCESS_TOKEN || !API_URL) {
|
|
13
|
+
throw new Error('ACCESS_TOKEN and API_URL are required in src/core/examples/.env');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const client = new EngineServicesClient(ACCESS_TOKEN!, API_URL!);
|
|
18
|
+
|
|
19
|
+
if (!PROJECT_ID) {
|
|
20
|
+
console.log('Set PROJECT_ID in .env to run this example.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create a test file to work with.
|
|
25
|
+
const blob = new Blob([JSON.stringify({ test: true })], { type: 'application/json' });
|
|
26
|
+
const fileName = `__example-test__-${Date.now()}.json`;
|
|
27
|
+
const created = await client.createFile({ file: blob, name: fileName, versionTag: 'v1', projectId: PROJECT_ID });
|
|
28
|
+
const fileId = created.item._id;
|
|
29
|
+
console.log(`\nCreated test file: [${fileId}] ${created.item.name}`);
|
|
30
|
+
|
|
31
|
+
// --- Get metadata (empty on a fresh version) ---
|
|
32
|
+
// Returns {} when the version exists but has no metadata yet — not null or 404.
|
|
33
|
+
const empty = await client.getFileVersionMetadata(fileId, 'v1');
|
|
34
|
+
console.log(`\nMetadata on v1 (fresh): ${JSON.stringify(empty)}`);
|
|
35
|
+
|
|
36
|
+
// --- Update metadata ---
|
|
37
|
+
const updated = await client.updateFileVersionMetadata(fileId, 'v1', {
|
|
38
|
+
discipline: 'structural',
|
|
39
|
+
approved: 'true',
|
|
40
|
+
});
|
|
41
|
+
console.log(`\nUpdated metadata: ${JSON.stringify(updated)}`);
|
|
42
|
+
|
|
43
|
+
// --- Get metadata (after update) ---
|
|
44
|
+
const meta = await client.getFileVersionMetadata(fileId, 'v1');
|
|
45
|
+
console.log(`\nMetadata on v1 (after update): ${JSON.stringify(meta)}`);
|
|
46
|
+
|
|
47
|
+
// --- Upload a second version and set metadata on it ---
|
|
48
|
+
const blobV2 = new Blob([JSON.stringify({ test: true, version: 2 })], { type: 'application/json' });
|
|
49
|
+
await client.updateFile(fileId, { file: blobV2, versionTag: 'v2' });
|
|
50
|
+
await client.updateFileVersionMetadata(fileId, 'v2', { discipline: 'architecture' });
|
|
51
|
+
console.log('\nUploaded v2 with its own metadata.');
|
|
52
|
+
|
|
53
|
+
// Metadata is per-version — each version holds its own independent metadata object.
|
|
54
|
+
const metaV1 = await client.getFileVersionMetadata(fileId, 'v1');
|
|
55
|
+
const metaV2 = await client.getFileVersionMetadata(fileId, 'v2');
|
|
56
|
+
console.log(`\nv1 metadata: ${JSON.stringify(metaV1)}`);
|
|
57
|
+
console.log(`v2 metadata: ${JSON.stringify(metaV2)}`);
|
|
58
|
+
|
|
59
|
+
// --- List versions ---
|
|
60
|
+
const versions = await client.listVersions(fileId);
|
|
61
|
+
console.log(`\nVersions (${versions.length}):`);
|
|
62
|
+
for (const v of versions) {
|
|
63
|
+
console.log(` [${v.tag}] created: ${v.createdAt}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --- Archive a version ---
|
|
67
|
+
// A version must be archived before it can be permanently deleted.
|
|
68
|
+
await client.archiveVersion(fileId, 'v1');
|
|
69
|
+
console.log('\nArchived v1.');
|
|
70
|
+
|
|
71
|
+
const versionsAfterArchive = await client.listVersions(fileId, { archived: false });
|
|
72
|
+
console.log(`Active versions after archiving v1: ${versionsAfterArchive.map(v => v.tag).join(', ')}`);
|
|
73
|
+
|
|
74
|
+
// --- Recover a version ---
|
|
75
|
+
await client.recoverVersion(fileId, 'v1');
|
|
76
|
+
console.log('Recovered v1.');
|
|
77
|
+
|
|
78
|
+
// --- Delete metadata ---
|
|
79
|
+
await client.deleteFileVersionMetadata(fileId, 'v1');
|
|
80
|
+
const afterDelete = await client.getFileVersionMetadata(fileId, 'v1');
|
|
81
|
+
console.log(`\nMetadata on v1 after delete: ${JSON.stringify(afterDelete)}`);
|
|
82
|
+
|
|
83
|
+
// --- Delete a version permanently ---
|
|
84
|
+
// archiveVersion first is required — deleteVersion on an active version returns an error.
|
|
85
|
+
await client.archiveVersion(fileId, 'v1');
|
|
86
|
+
await client.deleteVersion(fileId, 'v1');
|
|
87
|
+
console.log('\nPermanently deleted v1.');
|
|
88
|
+
|
|
89
|
+
// --- Cleanup ---
|
|
90
|
+
await client.archiveFile(fileId);
|
|
91
|
+
console.log(`\nArchived test file [${fileId}] — account is clean.`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// description: "File lifecycle — list, get with versions, create, rename, upload new version, download by version, archive, and recover."
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { EngineServicesClient } from '../client';
|
|
5
|
+
|
|
6
|
+
config({ path: resolve(__dirname, '.env') });
|
|
7
|
+
|
|
8
|
+
const ACCESS_TOKEN = process.env.ACCESS_TOKEN;
|
|
9
|
+
const API_URL = process.env.API_URL;
|
|
10
|
+
const PROJECT_ID = process.env.PROJECT_ID;
|
|
11
|
+
|
|
12
|
+
if (!ACCESS_TOKEN || !API_URL) {
|
|
13
|
+
throw new Error('ACCESS_TOKEN and API_URL are required in src/core/examples/.env');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const client = new EngineServicesClient(ACCESS_TOKEN!, API_URL!);
|
|
18
|
+
|
|
19
|
+
// --- List ---
|
|
20
|
+
const files = await client.listFiles();
|
|
21
|
+
console.log(`\nFound ${files.length} file(s):`);
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
console.log(` [${file._id}] ${file.name}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Get ---
|
|
27
|
+
if (files.length > 0) {
|
|
28
|
+
const detail = await client.getFile(files[0]._id, { showVersions: true });
|
|
29
|
+
console.log('\nFirst file detail (with versions):');
|
|
30
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!PROJECT_ID) {
|
|
34
|
+
console.log('\nSkipping create/update/download/archive — set PROJECT_ID in .env to run the full cycle.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Create ---
|
|
39
|
+
// projectId is required — the backend rejects uploads without one.
|
|
40
|
+
const fileName = `__example-test__-${Date.now()}.json`;
|
|
41
|
+
const blob = new Blob([JSON.stringify({ test: true })], { type: 'application/json' });
|
|
42
|
+
const created = await client.createFile({ file: blob, name: fileName, versionTag: 'v1', projectId: PROJECT_ID });
|
|
43
|
+
console.log(`\nCreated file: [${created.item._id}] ${created.item.name}`);
|
|
44
|
+
|
|
45
|
+
const fileId = created.item._id;
|
|
46
|
+
|
|
47
|
+
// --- Update (rename) ---
|
|
48
|
+
const renamed = await client.updateFile(fileId, { name: `${fileName}-renamed` });
|
|
49
|
+
console.log(`\nRenamed to: ${renamed.item?.name}`);
|
|
50
|
+
|
|
51
|
+
// --- Update (new version) ---
|
|
52
|
+
const blobV2 = new Blob([JSON.stringify({ test: true, version: 2 })], { type: 'application/json' });
|
|
53
|
+
const versioned = await client.updateFile(fileId, { file: blobV2, versionTag: 'v2' });
|
|
54
|
+
console.log(`\nUploaded new version: ${versioned.version?.tag}`);
|
|
55
|
+
|
|
56
|
+
// --- Download ---
|
|
57
|
+
// Without versionTag, always downloads the latest version — not necessarily the one from createFile.
|
|
58
|
+
const response = await client.downloadFile(fileId);
|
|
59
|
+
const text = await response.text();
|
|
60
|
+
console.log(`\nDownloaded content (latest version): ${text}`);
|
|
61
|
+
|
|
62
|
+
const responseV1 = await client.downloadFile(fileId, { versionTag: 'v1' });
|
|
63
|
+
const textV1 = await responseV1.text();
|
|
64
|
+
console.log(`Downloaded content (v1): ${textV1}`);
|
|
65
|
+
|
|
66
|
+
// --- Archive / Recover ---
|
|
67
|
+
// archiveFile is a soft-delete — the file can be recovered with recoverFile.
|
|
68
|
+
await client.archiveFile(fileId);
|
|
69
|
+
console.log(`\nArchived file [${fileId}]`);
|
|
70
|
+
|
|
71
|
+
// recoverFile returns no body — confirm by re-fetching the file.
|
|
72
|
+
await client.recoverFile(fileId);
|
|
73
|
+
const recovered = await client.getFile(fileId);
|
|
74
|
+
console.log(`Recovered file: ${recovered.name}`);
|
|
75
|
+
|
|
76
|
+
// Archive again to leave the account clean.
|
|
77
|
+
await client.archiveFile(fileId);
|
|
78
|
+
console.log(`Re-archived test file [${fileId}] — account is clean.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// description: "Folder lifecycle — list, get, create nested folders, list files inside a folder, rename, download as ZIP, archive, and recover."
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { EngineServicesClient } from '../client';
|
|
5
|
+
|
|
6
|
+
config({ path: resolve(__dirname, '.env') });
|
|
7
|
+
|
|
8
|
+
const ACCESS_TOKEN = process.env.ACCESS_TOKEN;
|
|
9
|
+
const API_URL = process.env.API_URL;
|
|
10
|
+
const PROJECT_ID = process.env.PROJECT_ID;
|
|
11
|
+
|
|
12
|
+
if (!ACCESS_TOKEN || !API_URL) {
|
|
13
|
+
throw new Error('ACCESS_TOKEN and API_URL are required in src/core/examples/.env');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const client = new EngineServicesClient(ACCESS_TOKEN!, API_URL!);
|
|
18
|
+
|
|
19
|
+
// --- List ---
|
|
20
|
+
const folders = await client.listFolders();
|
|
21
|
+
console.log(`\nFound ${folders.length} folder(s):`);
|
|
22
|
+
for (const folder of folders) {
|
|
23
|
+
console.log(` [${folder._id}] ${folder.name}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Get ---
|
|
27
|
+
if (folders.length > 0) {
|
|
28
|
+
const detail = await client.getFolder(folders[0]._id);
|
|
29
|
+
console.log('\nFirst folder detail:');
|
|
30
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!PROJECT_ID) {
|
|
34
|
+
console.log('\nSkipping create/update/download/archive — set PROJECT_ID in .env to run the full cycle.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Create (parent) ---
|
|
39
|
+
const folderName = `__example-test__-${Date.now()}`;
|
|
40
|
+
const parent = await client.createFolder(folderName, undefined, PROJECT_ID);
|
|
41
|
+
console.log(`\nCreated folder: [${parent._id}] ${parent.name}`);
|
|
42
|
+
|
|
43
|
+
// --- Create (nested) ---
|
|
44
|
+
// parentId nests the folder inside another — omitting it creates a root-level folder.
|
|
45
|
+
const child = await client.createFolder(`${folderName}-child`, parent._id, PROJECT_ID);
|
|
46
|
+
console.log(`Created nested folder: [${child._id}] ${child.name}`);
|
|
47
|
+
|
|
48
|
+
// --- List by parent ---
|
|
49
|
+
// NOTE: listFolders({ parentFolderId }) returned 0 even after creating a child with that parentId.
|
|
50
|
+
// The backend may not index parentFolderId for filtering, or the field name differs internally.
|
|
51
|
+
const children = await client.listFolders({ parentFolderId: parent._id });
|
|
52
|
+
console.log(`\nChildren of [${parent._id}]: ${children.length} folder(s)`);
|
|
53
|
+
|
|
54
|
+
// --- List files inside a folder ---
|
|
55
|
+
const filesInFolder = await client.listFiles({ folderId: parent._id });
|
|
56
|
+
console.log(`Files inside folder: ${filesInFolder.length}`);
|
|
57
|
+
|
|
58
|
+
// --- Update (rename) ---
|
|
59
|
+
const renamed = await client.updateFolder(parent._id, { name: `${folderName}-renamed` });
|
|
60
|
+
console.log(`\nRenamed to: ${renamed.name}`);
|
|
61
|
+
|
|
62
|
+
// --- Download as ZIP ---
|
|
63
|
+
const zip = await client.downloadFolder(parent._id);
|
|
64
|
+
const buffer = await zip.arrayBuffer();
|
|
65
|
+
console.log(`\nDownloaded folder ZIP: ${buffer.byteLength} bytes`);
|
|
66
|
+
|
|
67
|
+
// --- Archive / Recover ---
|
|
68
|
+
// archiveFolder is a soft-delete — recoverable with recoverFolder.
|
|
69
|
+
// Archiving a parent cascades to all children: sub-folders and their files are archived too.
|
|
70
|
+
await client.archiveFolder(child._id);
|
|
71
|
+
console.log(`\nArchived child folder [${child._id}]`);
|
|
72
|
+
|
|
73
|
+
await client.recoverFolder(child._id);
|
|
74
|
+
console.log(`Recovered child folder [${child._id}]`);
|
|
75
|
+
|
|
76
|
+
// Archive both to clean up.
|
|
77
|
+
await client.archiveFolder(child._id);
|
|
78
|
+
await client.archiveFolder(parent._id);
|
|
79
|
+
console.log(`Re-archived folders — account is clean.`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// description: "Hidden file lifecycle — create, get, list by parent, download, and delete."
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { EngineServicesClient } from '../client';
|
|
5
|
+
|
|
6
|
+
config({ path: resolve(__dirname, '.env') });
|
|
7
|
+
|
|
8
|
+
const ACCESS_TOKEN = process.env.ACCESS_TOKEN;
|
|
9
|
+
const API_URL = process.env.API_URL;
|
|
10
|
+
const PROJECT_ID = process.env.PROJECT_ID;
|
|
11
|
+
|
|
12
|
+
if (!ACCESS_TOKEN || !API_URL) {
|
|
13
|
+
throw new Error('ACCESS_TOKEN and API_URL are required in src/core/examples/.env');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const client = new EngineServicesClient(ACCESS_TOKEN!, API_URL!);
|
|
18
|
+
|
|
19
|
+
if (!PROJECT_ID) {
|
|
20
|
+
console.log('Set PROJECT_ID in .env to run this example.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Hidden files are always attached to a parent item — they have no standalone existence.
|
|
25
|
+
// Create a parent file to work with.
|
|
26
|
+
const parentBlob = new Blob([JSON.stringify({ test: true })], { type: 'application/json' });
|
|
27
|
+
const parentName = `__example-test__-${Date.now()}.json`;
|
|
28
|
+
const parent = await client.createFile({ file: parentBlob, name: parentName, versionTag: 'v1', projectId: PROJECT_ID });
|
|
29
|
+
const parentId = parent.item._id;
|
|
30
|
+
console.log(`\nCreated parent file: [${parentId}] ${parent.item.name}`);
|
|
31
|
+
|
|
32
|
+
// --- Create hidden files ---
|
|
33
|
+
const hiddenBlob1 = new Blob([JSON.stringify({ hidden: 1 })], { type: 'application/json' });
|
|
34
|
+
const hiddenBlob2 = new Blob([JSON.stringify({ hidden: 2 })], { type: 'application/json' });
|
|
35
|
+
|
|
36
|
+
const hidden1 = await client.createHiddenFile(hiddenBlob1, parentId);
|
|
37
|
+
console.log(`\nCreated hidden file 1: [${hidden1.hiddenFileId}]`);
|
|
38
|
+
|
|
39
|
+
const hidden2 = await client.createHiddenFile(hiddenBlob2, parentId);
|
|
40
|
+
console.log(`Created hidden file 2: [${hidden2.hiddenFileId}]`);
|
|
41
|
+
|
|
42
|
+
// --- Get ---
|
|
43
|
+
const fetched = await client.getHiddenFile(hidden1.hiddenFileId);
|
|
44
|
+
console.log(`\nHidden file 1 detail:`);
|
|
45
|
+
console.log(JSON.stringify(fetched, null, 2));
|
|
46
|
+
|
|
47
|
+
// --- List by parent ---
|
|
48
|
+
const allHidden = await client.getHiddenFilesByParent(parentId);
|
|
49
|
+
console.log(`\nHidden files attached to parent: ${allHidden.length}`);
|
|
50
|
+
for (const h of allHidden) {
|
|
51
|
+
console.log(` [${h._id}]`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Download ---
|
|
55
|
+
const response = await client.downloadHiddenFile(hidden1.hiddenFileId);
|
|
56
|
+
const text = await response.text();
|
|
57
|
+
console.log(`\nDownloaded hidden file 1: ${text}`);
|
|
58
|
+
|
|
59
|
+
// --- Delete one ---
|
|
60
|
+
await client.deleteHiddenFile(hidden1.hiddenFileId);
|
|
61
|
+
console.log(`\nDeleted hidden file 1 [${hidden1.hiddenFileId}]`);
|
|
62
|
+
|
|
63
|
+
// NOTE: getHiddenFilesByParent may still return deleted files immediately after deletion —
|
|
64
|
+
// the backend list appears to be eventually consistent.
|
|
65
|
+
const afterDelete = await client.getHiddenFilesByParent(parentId);
|
|
66
|
+
console.log(`Hidden files remaining: ${afterDelete.length}`);
|
|
67
|
+
|
|
68
|
+
// --- Delete all by parent ---
|
|
69
|
+
// deleteHiddenFilesByParent removes all hidden files attached to the parent in one call.
|
|
70
|
+
await client.deleteHiddenFilesByParent(parentId);
|
|
71
|
+
console.log(`\nDeleted all remaining hidden files for parent [${parentId}]`);
|
|
72
|
+
|
|
73
|
+
const afterDeleteAll = await client.getHiddenFilesByParent(parentId);
|
|
74
|
+
console.log(`Hidden files remaining: ${afterDeleteAll.length}`);
|
|
75
|
+
|
|
76
|
+
// --- Cleanup ---
|
|
77
|
+
await client.archiveFile(parentId);
|
|
78
|
+
console.log(`\nArchived parent file [${parentId}] — account is clean.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// description: "Item icon management — upload, download, and remove icons on any item (file, app, or component)."
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { EngineServicesClient } from '../client';
|
|
5
|
+
|
|
6
|
+
config({ path: resolve(__dirname, '.env') });
|
|
7
|
+
|
|
8
|
+
const ACCESS_TOKEN = process.env.ACCESS_TOKEN;
|
|
9
|
+
const API_URL = process.env.API_URL;
|
|
10
|
+
const PROJECT_ID = process.env.PROJECT_ID;
|
|
11
|
+
|
|
12
|
+
if (!ACCESS_TOKEN || !API_URL) {
|
|
13
|
+
throw new Error('ACCESS_TOKEN and API_URL are required in src/core/examples/.env');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const client = new EngineServicesClient(ACCESS_TOKEN!, API_URL!);
|
|
18
|
+
|
|
19
|
+
if (!PROJECT_ID) {
|
|
20
|
+
console.log('Set PROJECT_ID in .env to run this example.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create a test file to attach an icon to.
|
|
25
|
+
const blob = new Blob([JSON.stringify({ test: true })], { type: 'application/json' });
|
|
26
|
+
const fileName = `__example-test__-${Date.now()}.json`;
|
|
27
|
+
const created = await client.createFile({ file: blob, name: fileName, versionTag: 'v1', projectId: PROJECT_ID });
|
|
28
|
+
const itemId = created.item._id;
|
|
29
|
+
console.log(`\nCreated test file: [${itemId}] ${created.item.name}`);
|
|
30
|
+
|
|
31
|
+
// --- Upload icon ---
|
|
32
|
+
// Accepted formats: PNG, WebP, or ICO — max 512 KB.
|
|
33
|
+
// uploadItemIcon replaces any existing icon; there is no separate "update icon" method.
|
|
34
|
+
const iconPng = generateMinimalPng();
|
|
35
|
+
const updated = await client.uploadItemIcon(itemId, iconPng);
|
|
36
|
+
console.log(`\nUploaded icon. iconFileId: ${updated.iconFileId}, mimeType: ${updated.iconMimeType}`);
|
|
37
|
+
|
|
38
|
+
// --- Download icon ---
|
|
39
|
+
const iconResponse = await client.getItemIcon(itemId);
|
|
40
|
+
const iconBuffer = await iconResponse.arrayBuffer();
|
|
41
|
+
console.log(`\nDownloaded icon: ${iconBuffer.byteLength} bytes`);
|
|
42
|
+
|
|
43
|
+
// --- Remove icon ---
|
|
44
|
+
const removed = await client.removeItemIcon(itemId);
|
|
45
|
+
console.log(`\nRemoved icon. iconFileId: ${removed.iconFileId ?? 'null'}`);
|
|
46
|
+
|
|
47
|
+
// --- Cleanup ---
|
|
48
|
+
await client.archiveFile(itemId);
|
|
49
|
+
console.log(`\nArchived test file [${itemId}] — account is clean.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Generates the smallest valid PNG (1x1 transparent pixel) as a Blob.
|
|
53
|
+
function generateMinimalPng(): Blob {
|
|
54
|
+
const bytes = new Uint8Array([
|
|
55
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature
|
|
56
|
+
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, // IHDR chunk length + type
|
|
57
|
+
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // width=1, height=1
|
|
58
|
+
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, // bit depth=8, colorType=6 (RGBA)
|
|
59
|
+
0x89, 0x00, 0x00, 0x00, 0x0b, 0x49, 0x44, 0x41, // IHDR CRC + IDAT chunk
|
|
60
|
+
0x54, 0x78, 0x9c, 0x62, 0x00, 0x00, 0x00, 0x02, // IDAT data (zlib compressed)
|
|
61
|
+
0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, // IDAT CRC
|
|
62
|
+
0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, // IEND chunk
|
|
63
|
+
0x60, 0x82,
|
|
64
|
+
]);
|
|
65
|
+
return new Blob([bytes], { type: 'image/png' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// description: "Permission checks — single and batch permission verification using PlatformClient (requires user JWT, not API token)."
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { PlatformClient } from '../platform-client';
|
|
5
|
+
|
|
6
|
+
config({ path: resolve(__dirname, '.env') });
|
|
7
|
+
|
|
8
|
+
const API_URL = process.env.API_URL;
|
|
9
|
+
const PROJECT_ID = process.env.PROJECT_ID;
|
|
10
|
+
|
|
11
|
+
// PlatformClient requires a user JWT — not the API token used by EngineServicesClient.
|
|
12
|
+
// To run this example:
|
|
13
|
+
// 1. Open the platform in your browser and log in.
|
|
14
|
+
// 2. Open DevTools → Network → click any /api request.
|
|
15
|
+
// 3. Copy the Authorization header value (without the "Bearer " prefix).
|
|
16
|
+
// 4. Set it as JWT in src/core/examples/.env.
|
|
17
|
+
const JWT = process.env.JWT;
|
|
18
|
+
|
|
19
|
+
if (!API_URL) {
|
|
20
|
+
throw new Error('API_URL is required in src/core/examples/.env');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!JWT) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'JWT is required to run permissions.ts.\n' +
|
|
26
|
+
'Get it from DevTools → Network → any /api request → Authorization header (without "Bearer ").\n' +
|
|
27
|
+
'Set it as JWT in src/core/examples/.env.',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!PROJECT_ID) {
|
|
32
|
+
throw new Error('PROJECT_ID is required in src/core/examples/.env');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function main() {
|
|
36
|
+
// PlatformClient extends EngineServicesClient and forces useBearer: true.
|
|
37
|
+
// It owns the JWT-only routes — checkPermission, checkPermissionBatch, getProject, getProjectData.
|
|
38
|
+
// These routes are guarded server-side and are not reachable with an API token.
|
|
39
|
+
const client = new PlatformClient(JWT!, API_URL!);
|
|
40
|
+
|
|
41
|
+
// --- Single permission check ---
|
|
42
|
+
const result = await client.checkPermission({
|
|
43
|
+
resourceType: 'STORAGE',
|
|
44
|
+
action: 'READ',
|
|
45
|
+
projectId: PROJECT_ID,
|
|
46
|
+
});
|
|
47
|
+
console.log('\nSTORAGE:READ check:');
|
|
48
|
+
console.log(` hasPermission: ${result.hasPermission}`);
|
|
49
|
+
// scope indicates HOW the permission was granted:
|
|
50
|
+
// 'global' — admin/owner bypass
|
|
51
|
+
// 'project' — role-level grant on the project
|
|
52
|
+
// 'entity' — per-entity override
|
|
53
|
+
// 'none' — denied
|
|
54
|
+
console.log(` scope: ${result.scope}`);
|
|
55
|
+
|
|
56
|
+
// --- Batch permission check ---
|
|
57
|
+
// Evaluates multiple checks in a single round-trip — useful to hydrate
|
|
58
|
+
// action visibility for many rows without N+1 calls.
|
|
59
|
+
const checks = [
|
|
60
|
+
{ resourceType: 'STORAGE', action: 'READ', projectId: PROJECT_ID },
|
|
61
|
+
{ resourceType: 'STORAGE', action: 'WRITE', projectId: PROJECT_ID },
|
|
62
|
+
{ resourceType: 'STORAGE', action: 'DELETE', projectId: PROJECT_ID },
|
|
63
|
+
];
|
|
64
|
+
const results = await client.checkPermissionBatch(checks);
|
|
65
|
+
console.log('\nBatch permission check:');
|
|
66
|
+
for (let i = 0; i < checks.length; i++) {
|
|
67
|
+
const { resourceType, action } = checks[i];
|
|
68
|
+
const { hasPermission, scope } = results[i];
|
|
69
|
+
console.log(` ${resourceType}:${action} → hasPermission: ${hasPermission}, scope: ${scope}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- Get project ---
|
|
73
|
+
const project = await client.getProject(PROJECT_ID!);
|
|
74
|
+
console.log('\nProject:');
|
|
75
|
+
console.log(JSON.stringify(project, null, 2));
|
|
76
|
+
|
|
77
|
+
// --- Get project data ---
|
|
78
|
+
// getProjectData returns the full project snapshot: users, roles, files, folders.
|
|
79
|
+
// User data is stripped of sensitive fields server-side.
|
|
80
|
+
const data = await client.getProjectData(PROJECT_ID!);
|
|
81
|
+
console.log('\nProject data (summary):');
|
|
82
|
+
console.log(` users: ${data.users?.length ?? 0}`);
|
|
83
|
+
console.log(` files: ${data.files?.length ?? 0}`);
|
|
84
|
+
console.log(` folders: ${data.folders?.length ?? 0}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main().catch(console.error);
|