dalux-build-api 1.1.3 → 1.1.5

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.
@@ -1,24 +1,39 @@
1
- 'use strict';
2
-
3
- /**
4
- * Configuration for the Dalux Build API client.
5
- */
6
- class Configuration {
7
- /**
8
- * @param {object} options
9
- * @param {string} options.baseUrl - The API base URL provided by Dalux (e.g. https://api.dalux.com/build)
10
- * @param {string} options.apiKey - Your company-specific X-API-KEY
11
- */
12
- constructor({ baseUrl, apiKey } = {}) {
13
- if (!baseUrl) {
14
- throw new Error('baseUrl is required');
15
- }
16
- if (!apiKey) {
17
- throw new Error('apiKey is required');
18
- }
19
- this.baseUrl = baseUrl.replace(/\/$/, '');
20
- this.apiKey = apiKey;
21
- }
22
- }
23
-
24
- module.exports = Configuration;
1
+ 'use strict';
2
+
3
+ // Load .env file when dotenv is available (optional peer dep)
4
+ try {
5
+ require('dotenv').config();
6
+ } catch (_) { /* dotenv not installed – skip */ }
7
+
8
+ /**
9
+ * Configuration for the Dalux Build API client.
10
+ *
11
+ * When *baseUrl* or *apiKey* are not provided they are read from the
12
+ * environment variables ``DALUX_BASE_URL`` and ``DALUX_API_KEY`` respectively,
13
+ * matching the behaviour of the Python client.
14
+ */
15
+ class Configuration {
16
+ /**
17
+ * @param {object} [options]
18
+ * @param {string} [options.baseUrl] - The API base URL provided by Dalux
19
+ * (e.g. ``https://<company>.dalux.com/api``).
20
+ * Falls back to the ``DALUX_BASE_URL`` environment variable.
21
+ * @param {string} [options.apiKey] - Your company-specific X-API-KEY.
22
+ * Falls back to the ``DALUX_API_KEY`` environment variable.
23
+ */
24
+ constructor({ baseUrl, apiKey } = {}) {
25
+ const resolvedBaseUrl = baseUrl || process.env.DALUX_BASE_URL;
26
+ const resolvedApiKey = apiKey || process.env.DALUX_API_KEY;
27
+
28
+ if (!resolvedBaseUrl) {
29
+ throw new Error('baseUrl is required (or set DALUX_BASE_URL env var)');
30
+ }
31
+ if (!resolvedApiKey) {
32
+ throw new Error('apiKey is required (or set DALUX_API_KEY env var)');
33
+ }
34
+ this.baseUrl = resolvedBaseUrl.replace(/\/$/, '');
35
+ this.apiKey = resolvedApiKey;
36
+ }
37
+ }
38
+
39
+ module.exports = Configuration;
package/src/index.js CHANGED
@@ -1,92 +1,128 @@
1
- 'use strict';
2
-
3
- const Configuration = require('./configuration');
4
- const ApiClient = require('./apiClient');
5
-
6
- const CompaniesApi = require('./api/CompaniesApi');
7
- const CompanyCatalogApi = require('./api/CompanyCatalogApi');
8
- const FileAreasApi = require('./api/FileAreasApi');
9
- const FileRevisionsApi = require('./api/FileRevisionsApi');
10
- const FileUploadApi = require('./api/FileUploadApi');
11
- const FilesApi = require('./api/FilesApi');
12
- const FoldersApi = require('./api/FoldersApi');
13
- const FormsApi = require('./api/FormsApi');
14
- const InspectionPlansApi = require('./api/InspectionPlansApi');
15
- const ProjectTemplatesApi = require('./api/ProjectTemplatesApi');
16
- const ProjectsApi = require('./api/ProjectsApi');
17
- const TasksApi = require('./api/TasksApi');
18
- const TestPlansApi = require('./api/TestPlansApi');
19
- const UsersApi = require('./api/UsersApi');
20
- const VersionSetsApi = require('./api/VersionSetsApi');
21
- const WorkPackagesApi = require('./api/WorkPackagesApi');
22
-
23
- /**
24
- * Create a fully configured Dalux Build API client.
25
- *
26
- * @param {object} options
27
- * @param {string} options.baseUrl - The API base URL (obtain from Dalux support)
28
- * @param {string} options.apiKey - Your X-API-KEY (manage via Dalux Settings › Integrations › API Identities)
29
- * @returns {{
30
- * projects: ProjectsApi,
31
- * companies: CompaniesApi,
32
- * companyCatalog: CompanyCatalogApi,
33
- * fileAreas: FileAreasApi,
34
- * fileRevisions: FileRevisionsApi,
35
- * fileUpload: FileUploadApi,
36
- * files: FilesApi,
37
- * folders: FoldersApi,
38
- * forms: FormsApi,
39
- * inspectionPlans: InspectionPlansApi,
40
- * projectTemplates: ProjectTemplatesApi,
41
- * tasks: TasksApi,
42
- * testPlans: TestPlansApi,
43
- * users: UsersApi,
44
- * versionSets: VersionSetsApi,
45
- * workPackages: WorkPackagesApi
46
- * }}
47
- */
48
- function createClient({ baseUrl, apiKey } = {}) {
49
- const configuration = new Configuration({ baseUrl, apiKey });
50
- const apiClient = new ApiClient(configuration);
51
-
52
- return {
53
- projects: new ProjectsApi(apiClient),
54
- companies: new CompaniesApi(apiClient),
55
- companyCatalog: new CompanyCatalogApi(apiClient),
56
- fileAreas: new FileAreasApi(apiClient),
57
- fileRevisions: new FileRevisionsApi(apiClient),
58
- fileUpload: new FileUploadApi(apiClient),
59
- files: new FilesApi(apiClient),
60
- folders: new FoldersApi(apiClient),
61
- forms: new FormsApi(apiClient),
62
- inspectionPlans: new InspectionPlansApi(apiClient),
63
- projectTemplates: new ProjectTemplatesApi(apiClient),
64
- tasks: new TasksApi(apiClient),
65
- testPlans: new TestPlansApi(apiClient),
66
- users: new UsersApi(apiClient),
67
- versionSets: new VersionSetsApi(apiClient),
68
- workPackages: new WorkPackagesApi(apiClient),
69
- };
70
- }
71
-
72
- module.exports = {
73
- createClient,
74
- Configuration,
75
- ApiClient,
76
- CompaniesApi,
77
- CompanyCatalogApi,
78
- FileAreasApi,
79
- FileRevisionsApi,
80
- FileUploadApi,
81
- FilesApi,
82
- FoldersApi,
83
- FormsApi,
84
- InspectionPlansApi,
85
- ProjectTemplatesApi,
86
- ProjectsApi,
87
- TasksApi,
88
- TestPlansApi,
89
- UsersApi,
90
- VersionSetsApi,
91
- WorkPackagesApi,
92
- };
1
+ 'use strict';
2
+
3
+ const Configuration = require('./configuration');
4
+ const ApiClient = require('./apiClient');
5
+
6
+ const CompaniesApi = require('./api/CompaniesApi');
7
+ const CompanyCatalogApi = require('./api/CompanyCatalogApi');
8
+ const FileAreasApi = require('./api/FileAreasApi');
9
+ const FileRevisionsApi = require('./api/FileRevisionsApi');
10
+ const FileUploadApi = require('./api/FileUploadApi');
11
+ const FilesApi = require('./api/FilesApi');
12
+ const FoldersApi = require('./api/FoldersApi');
13
+ const FormsApi = require('./api/FormsApi');
14
+ const InspectionPlansApi = require('./api/InspectionPlansApi');
15
+ const ProjectTemplatesApi = require('./api/ProjectTemplatesApi');
16
+ const ProjectsApi = require('./api/ProjectsApi');
17
+ const TasksApi = require('./api/TasksApi');
18
+ const TestPlansApi = require('./api/TestPlansApi');
19
+ const UsersApi = require('./api/UsersApi');
20
+ const VersionSetsApi = require('./api/VersionSetsApi');
21
+ const WorkPackagesApi = require('./api/WorkPackagesApi');
22
+
23
+ const {
24
+ DaluxError,
25
+ NotFoundError,
26
+ ApiError,
27
+ ValidationError,
28
+ AuthenticationError,
29
+ RateLimitError,
30
+ hasNextPage,
31
+ getNextBookmark,
32
+ paginate,
33
+ findByField,
34
+ findAllByField,
35
+ validateProjectId,
36
+ validateFileAreaId,
37
+ validateFolderId,
38
+ resolveFileAreaByName,
39
+ resolveFolderIdFromNamedPath,
40
+ } = require('./utils');
41
+
42
+ /**
43
+ * Create a fully configured Dalux Build API client.
44
+ *
45
+ * @param {object} [options]
46
+ * @param {string} [options.baseUrl] - The API base URL (falls back to DALUX_BASE_URL env var)
47
+ * @param {string} [options.apiKey] - Your X-API-KEY (falls back to DALUX_API_KEY env var)
48
+ * @returns {{
49
+ * projects: ProjectsApi,
50
+ * companies: CompaniesApi,
51
+ * companyCatalog: CompanyCatalogApi,
52
+ * fileAreas: FileAreasApi,
53
+ * fileRevisions: FileRevisionsApi,
54
+ * fileUpload: FileUploadApi,
55
+ * files: FilesApi,
56
+ * folders: FoldersApi,
57
+ * forms: FormsApi,
58
+ * inspectionPlans: InspectionPlansApi,
59
+ * projectTemplates: ProjectTemplatesApi,
60
+ * tasks: TasksApi,
61
+ * testPlans: TestPlansApi,
62
+ * users: UsersApi,
63
+ * versionSets: VersionSetsApi,
64
+ * workPackages: WorkPackagesApi
65
+ * }}
66
+ */
67
+ function createClient({ baseUrl, apiKey } = {}) {
68
+ const configuration = new Configuration({ baseUrl, apiKey });
69
+ const apiClient = new ApiClient(configuration);
70
+
71
+ return {
72
+ projects: new ProjectsApi(apiClient),
73
+ companies: new CompaniesApi(apiClient),
74
+ companyCatalog: new CompanyCatalogApi(apiClient),
75
+ fileAreas: new FileAreasApi(apiClient),
76
+ fileRevisions: new FileRevisionsApi(apiClient),
77
+ fileUpload: new FileUploadApi(apiClient),
78
+ files: new FilesApi(apiClient),
79
+ folders: new FoldersApi(apiClient),
80
+ forms: new FormsApi(apiClient),
81
+ inspectionPlans: new InspectionPlansApi(apiClient),
82
+ projectTemplates: new ProjectTemplatesApi(apiClient),
83
+ tasks: new TasksApi(apiClient),
84
+ testPlans: new TestPlansApi(apiClient),
85
+ users: new UsersApi(apiClient),
86
+ versionSets: new VersionSetsApi(apiClient),
87
+ workPackages: new WorkPackagesApi(apiClient),
88
+ };
89
+ }
90
+
91
+ module.exports = {
92
+ createClient,
93
+ Configuration,
94
+ ApiClient,
95
+ CompaniesApi,
96
+ CompanyCatalogApi,
97
+ FileAreasApi,
98
+ FileRevisionsApi,
99
+ FileUploadApi,
100
+ FilesApi,
101
+ FoldersApi,
102
+ FormsApi,
103
+ InspectionPlansApi,
104
+ ProjectTemplatesApi,
105
+ ProjectsApi,
106
+ TasksApi,
107
+ TestPlansApi,
108
+ UsersApi,
109
+ VersionSetsApi,
110
+ WorkPackagesApi,
111
+ // Utilities
112
+ DaluxError,
113
+ NotFoundError,
114
+ ApiError,
115
+ ValidationError,
116
+ AuthenticationError,
117
+ RateLimitError,
118
+ hasNextPage,
119
+ getNextBookmark,
120
+ paginate,
121
+ findByField,
122
+ findAllByField,
123
+ validateProjectId,
124
+ validateFileAreaId,
125
+ validateFolderId,
126
+ resolveFileAreaByName,
127
+ resolveFolderIdFromNamedPath,
128
+ };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Base exception for all Dalux API errors.
5
+ */
6
+ class DaluxError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = this.constructor.name;
10
+ }
11
+ }
12
+
13
+ /**
14
+ * Resource not found (HTTP 404).
15
+ */
16
+ class NotFoundError extends DaluxError {}
17
+
18
+ /**
19
+ * API request failed (4xx / 5xx other than 401, 404, 429).
20
+ */
21
+ class ApiError extends DaluxError {}
22
+
23
+ /**
24
+ * Input validation failed.
25
+ */
26
+ class ValidationError extends DaluxError {}
27
+
28
+ /**
29
+ * Authentication failed (HTTP 401).
30
+ */
31
+ class AuthenticationError extends DaluxError {}
32
+
33
+ /**
34
+ * Rate limit exceeded (HTTP 429).
35
+ */
36
+ class RateLimitError extends DaluxError {}
37
+
38
+ module.exports = {
39
+ DaluxError,
40
+ NotFoundError,
41
+ ApiError,
42
+ ValidationError,
43
+ AuthenticationError,
44
+ RateLimitError,
45
+ };
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const { DaluxError, NotFoundError, ApiError, ValidationError, AuthenticationError, RateLimitError } = require('./errors');
4
+ const { hasNextPage, getNextBookmark, paginate } = require('./pagination');
5
+ const { findByField, findAllByField } = require('./search');
6
+ const { validateProjectId, validateFileAreaId, validateFolderId } = require('./validation');
7
+ const { resolveFileAreaByName, resolveFolderIdFromNamedPath } = require('./pathResolver');
8
+
9
+ module.exports = {
10
+ DaluxError,
11
+ NotFoundError,
12
+ ApiError,
13
+ ValidationError,
14
+ AuthenticationError,
15
+ RateLimitError,
16
+ hasNextPage,
17
+ getNextBookmark,
18
+ paginate,
19
+ findByField,
20
+ findAllByField,
21
+ validateProjectId,
22
+ validateFileAreaId,
23
+ validateFolderId,
24
+ resolveFileAreaByName,
25
+ resolveFolderIdFromNamedPath,
26
+ };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Check whether an API response has a next page link.
5
+ * @param {object} response
6
+ * @returns {boolean}
7
+ */
8
+ function hasNextPage(response) {
9
+ if (!response) return false;
10
+ const links = response.links || [];
11
+ return links.some((l) => l.rel === 'nextPage');
12
+ }
13
+
14
+ /**
15
+ * Extract the bookmark for the next page from an API response.
16
+ * @param {object} response
17
+ * @returns {string|null}
18
+ */
19
+ function getNextBookmark(response) {
20
+ const links = (response && response.links) || [];
21
+ const nextLink = links.find((l) => l.rel === 'nextPage');
22
+ if (!nextLink) return null;
23
+ try {
24
+ return new URL(nextLink.href).searchParams.get('bookmark');
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Generic pagination handler: follows bookmark pages until exhausted.
32
+ *
33
+ * @param {string} endpoint - API path.
34
+ * @param {object} client - ApiClient instance.
35
+ * @param {object} [params] - Base query parameters.
36
+ * @param {boolean} [verbose=false] - Log progress to console.
37
+ * @param {string} [itemAccessor='items'] - Key to read items from each response.
38
+ * @returns {Promise<any[]>} All items across all pages.
39
+ */
40
+ async function paginate(endpoint, client, params = {}, verbose = false, itemAccessor = 'items') {
41
+ const allItems = [];
42
+ let currentParams = { ...params };
43
+ let pageCount = 0;
44
+ const seenBookmarks = new Set();
45
+
46
+ while (true) {
47
+ pageCount += 1;
48
+ const response = await client.get(endpoint, currentParams);
49
+ if (!response) break;
50
+
51
+ const items = response[itemAccessor] || [];
52
+ allItems.push(...items);
53
+
54
+ if (verbose) {
55
+ const meta = response.metadata || {};
56
+ const remaining = meta.totalRemainingItems ?? 0;
57
+ console.log(
58
+ `Page ${pageCount}: ${items.length} items, Total: ${allItems.length}, Remaining: ${remaining}`,
59
+ );
60
+ }
61
+
62
+ if (!hasNextPage(response)) break;
63
+
64
+ const bookmark = getNextBookmark(response);
65
+ if (!bookmark) break;
66
+ if (seenBookmarks.has(bookmark)) {
67
+ if (verbose) {
68
+ console.log(`Detected duplicate bookmark '${bookmark}', stopping pagination to prevent infinite loop`);
69
+ }
70
+ break;
71
+ }
72
+ seenBookmarks.add(bookmark);
73
+ currentParams = { ...params, bookmark };
74
+ }
75
+
76
+ if (verbose) {
77
+ console.log(`Pagination complete. Total items retrieved: ${allItems.length}`);
78
+ }
79
+ return allItems;
80
+ }
81
+
82
+ module.exports = { hasNextPage, getNextBookmark, paginate };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const { findByField } = require('./search');
4
+
5
+ /**
6
+ * Resolve a file area by its displayed name for a project.
7
+ *
8
+ * @param {object} apiClient - ApiClient instance.
9
+ * @param {string} projectId
10
+ * @param {string} fileAreaName
11
+ * @param {object} [fileAreasCache] - Optional mutable cache {name -> fileArea item}.
12
+ * @returns {Promise<object|null>} File area item or null.
13
+ */
14
+ async function resolveFileAreaByName(apiClient, projectId, fileAreaName, fileAreasCache) {
15
+ if (fileAreasCache && fileAreasCache[fileAreaName]) {
16
+ return fileAreasCache[fileAreaName];
17
+ }
18
+
19
+ // Inline import to avoid circular deps
20
+ const FileAreasApi = require('../api/FileAreasApi');
21
+ const response = await new FileAreasApi(apiClient).getFileAreas(projectId);
22
+ const items = (response && response.items) || [];
23
+
24
+ if (fileAreasCache) {
25
+ for (const item of items) {
26
+ const name = (item.data || {}).fileAreaName || (item.data || {}).name || item.fileAreaName || item.name;
27
+ if (name) fileAreasCache[name] = item;
28
+ }
29
+ }
30
+
31
+ return (
32
+ findByField(items, 'fileAreaName', fileAreaName, (x) => x.data || x) ||
33
+ findByField(items, 'name', fileAreaName, (x) => x.data || x)
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Resolve "FileAreaName/Folder/SubFolder" to { fileAreaId, folderId }.
39
+ *
40
+ * @param {object} apiClient - ApiClient instance.
41
+ * @param {string} projectId
42
+ * @param {string} path - e.g. "Files/4_Design/C07_Geometry"
43
+ * @param {object} [opts]
44
+ * @param {boolean} [opts.verbose=false]
45
+ * @param {object} [opts.fileAreasCache]
46
+ * @param {object} [opts.foldersCache]
47
+ * @param {object} [opts.resolvedPathsCache]
48
+ * @returns {Promise<{ fileAreaId: string|null, folderId: string|null }>}
49
+ */
50
+ async function resolveFolderIdFromNamedPath(apiClient, projectId, path, opts = {}) {
51
+ const { verbose = false, fileAreasCache, foldersCache, resolvedPathsCache } = opts;
52
+
53
+ if (resolvedPathsCache && resolvedPathsCache[path]) {
54
+ return resolvedPathsCache[path];
55
+ }
56
+
57
+ const parts = path.split('/').map((p) => p.trim()).filter(Boolean);
58
+ if (parts.length < 2) return { fileAreaId: null, folderId: null };
59
+
60
+ const fileAreaName = parts[0];
61
+ const folderNames = parts.slice(1);
62
+
63
+ const fileAreaItem = await resolveFileAreaByName(apiClient, projectId, fileAreaName, fileAreasCache);
64
+ if (!fileAreaItem) {
65
+ if (verbose) console.log(`Could not resolve file area: ${fileAreaName}`);
66
+ return { fileAreaId: null, folderId: null };
67
+ }
68
+
69
+ const fileAreaId =
70
+ (fileAreaItem.data || {}).fileAreaId ||
71
+ (fileAreaItem.data || {}).id ||
72
+ fileAreaItem.fileAreaId ||
73
+ fileAreaItem.id;
74
+
75
+ // Fetch all folders for this file area (with cache)
76
+ let folders = foldersCache && foldersCache[fileAreaId];
77
+ if (!folders) {
78
+ if (verbose) {
79
+ console.log(`GET /5.1/projects/${projectId}/file_areas/${fileAreaId}/folders`);
80
+ }
81
+ const FoldersApi = require('../api/FoldersApi');
82
+ folders = await new FoldersApi(apiClient).getAllFolders(projectId, fileAreaId);
83
+ if (foldersCache) foldersCache[fileAreaId] = folders;
84
+ }
85
+
86
+ // Build a lookup: (parentFolderId, folderName) -> folder
87
+ // Collect all valid folder IDs first
88
+ const allFolderIds = new Set();
89
+ for (const f of folders) {
90
+ const data = f.data || f;
91
+ const fid = data.folderId || data.id;
92
+ if (fid) allFolderIds.add(fid);
93
+ }
94
+
95
+ const folderIndex = new Map();
96
+ for (const f of folders) {
97
+ const data = f.data || f;
98
+ const fid = data.folderId || data.id;
99
+ const pid = data.parentFolderId || data.parentId || null;
100
+ const name = data.folderName || data.name || '';
101
+ // Treat root-level folders (parent not in folder list) as children of null
102
+ const parentKey = allFolderIds.has(pid) ? pid : null;
103
+ folderIndex.set(`${parentKey}|||${name}`, fid);
104
+ }
105
+
106
+ let parentFolderId = null;
107
+ for (const folderName of folderNames) {
108
+ const key = `${parentFolderId}|||${folderName}`;
109
+ const fid = folderIndex.get(key);
110
+ if (!fid) {
111
+ if (verbose) console.log(`Could not resolve folder segment: ${folderName}`);
112
+ const result = { fileAreaId, folderId: null };
113
+ if (resolvedPathsCache) resolvedPathsCache[path] = result;
114
+ return result;
115
+ }
116
+ parentFolderId = fid;
117
+ }
118
+
119
+ const result = { fileAreaId, folderId: parentFolderId };
120
+ if (resolvedPathsCache) resolvedPathsCache[path] = result;
121
+ return result;
122
+ }
123
+
124
+ module.exports = { resolveFileAreaByName, resolveFolderIdFromNamedPath };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Find the first item in an iterable where a field equals a value.
5
+ *
6
+ * @param {any[]} items
7
+ * @param {string} field - Field name (supports camelCase and snake_case).
8
+ * @param {any} value - Value to match.
9
+ * @param {Function} [accessor] - Optional function to extract data from each item.
10
+ * @returns {any|null}
11
+ */
12
+ function findByField(items, field, value, accessor) {
13
+ const get = accessor || ((x) => x);
14
+ for (const item of items) {
15
+ const itemData = get(item);
16
+ if (itemData && typeof itemData === 'object') {
17
+ if (itemData[field] === value) return item;
18
+ // Also try snake_case ↔ camelCase conversion
19
+ const camel = snakeToCamel(field);
20
+ if (camel !== field && itemData[camel] === value) return item;
21
+ const snake = camelToSnake(field);
22
+ if (snake !== field && itemData[snake] === value) return item;
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+
28
+ /**
29
+ * Find all items in an iterable where a field equals a value.
30
+ *
31
+ * @param {any[]} items
32
+ * @param {string} field - Field name.
33
+ * @param {any} value - Value to match.
34
+ * @param {Function} [accessor] - Optional function to extract data from each item.
35
+ * @returns {any[]}
36
+ */
37
+ function findAllByField(items, field, value, accessor) {
38
+ const get = accessor || ((x) => x);
39
+ return items.filter((item) => {
40
+ const itemData = get(item);
41
+ if (!itemData || typeof itemData !== 'object') return false;
42
+ if (itemData[field] === value) return true;
43
+ const camel = snakeToCamel(field);
44
+ if (camel !== field && itemData[camel] === value) return true;
45
+ const snake = camelToSnake(field);
46
+ if (snake !== field && itemData[snake] === value) return true;
47
+ return false;
48
+ });
49
+ }
50
+
51
+ /** @param {string} s */
52
+ function snakeToCamel(s) {
53
+ return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
54
+ }
55
+
56
+ /** @param {string} s */
57
+ function camelToSnake(s) {
58
+ return s.replace(/([A-Z])/g, (c) => `_${c.toLowerCase()}`);
59
+ }
60
+
61
+ module.exports = { findByField, findAllByField };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const { ValidationError } = require('./errors');
4
+
5
+ /**
6
+ * Validate that projectId is a non-empty string.
7
+ * @param {string} projectId
8
+ */
9
+ function validateProjectId(projectId) {
10
+ if (!projectId || typeof projectId !== 'string' || projectId.trim().length === 0) {
11
+ throw new ValidationError('projectId must be a non-empty string');
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Validate that fileAreaId is a non-empty string.
17
+ * @param {string} fileAreaId
18
+ */
19
+ function validateFileAreaId(fileAreaId) {
20
+ if (!fileAreaId || typeof fileAreaId !== 'string' || fileAreaId.trim().length === 0) {
21
+ throw new ValidationError('fileAreaId must be a non-empty string');
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Validate that folderId is a non-empty string (or null/undefined).
27
+ * @param {string|null|undefined} folderId
28
+ */
29
+ function validateFolderId(folderId) {
30
+ if (folderId == null) return;
31
+ if (typeof folderId !== 'string' || folderId.trim().length === 0) {
32
+ throw new ValidationError('folderId must be a non-empty string');
33
+ }
34
+ }
35
+
36
+ module.exports = { validateProjectId, validateFileAreaId, validateFolderId };