@thatopen/services 0.16.0 → 0.17.0

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.
@@ -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,88 @@
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
+ // --- Tags ---
43
+ // The tag is optional on upload; a batch upload applies one tag to every file in it.
44
+ // Filtering by tag returns that tag's files in creation order.
45
+ const tagged = await client.createHiddenFile(hiddenBlob1, parentId, 'example:tagged');
46
+ const taggedList = await client.getHiddenFilesByParent(parentId, 'example:tagged');
47
+ console.log(`\nHidden files tagged 'example:tagged': ${taggedList.length} (first: [${tagged.hiddenFileId}] tag=${taggedList[0]?.tag})`);
48
+
49
+ // --- Get ---
50
+ const fetched = await client.getHiddenFile(hidden1.hiddenFileId);
51
+ console.log(`\nHidden file 1 detail:`);
52
+ console.log(JSON.stringify(fetched, null, 2));
53
+
54
+ // --- List by parent ---
55
+ const allHidden = await client.getHiddenFilesByParent(parentId);
56
+ console.log(`\nHidden files attached to parent: ${allHidden.length}`);
57
+ for (const h of allHidden) {
58
+ console.log(` [${h._id}]`);
59
+ }
60
+
61
+ // --- Download ---
62
+ const response = await client.downloadHiddenFile(hidden1.hiddenFileId);
63
+ const text = await response.text();
64
+ console.log(`\nDownloaded hidden file 1: ${text}`);
65
+
66
+ // --- Delete one ---
67
+ await client.deleteHiddenFile(hidden1.hiddenFileId);
68
+ console.log(`\nDeleted hidden file 1 [${hidden1.hiddenFileId}]`);
69
+
70
+ // NOTE: getHiddenFilesByParent may still return deleted files immediately after deletion —
71
+ // the backend list appears to be eventually consistent.
72
+ const afterDelete = await client.getHiddenFilesByParent(parentId);
73
+ console.log(`Hidden files remaining: ${afterDelete.length}`);
74
+
75
+ // --- Delete all by parent ---
76
+ // deleteHiddenFilesByParent removes all hidden files attached to the parent in one call.
77
+ await client.deleteHiddenFilesByParent(parentId);
78
+ console.log(`\nDeleted all remaining hidden files for parent [${parentId}]`);
79
+
80
+ const afterDeleteAll = await client.getHiddenFilesByParent(parentId);
81
+ console.log(`Hidden files remaining: ${afterDeleteAll.length}`);
82
+
83
+ // --- Cleanup ---
84
+ await client.archiveFile(parentId);
85
+ console.log(`\nArchived parent file [${parentId}] — account is clean.`);
86
+ }
87
+
88
+ 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);