Skip to content

Commit d4cb481

Browse files
committed
refactor: migrate environmentId to projectId across metadata and objectql packages
- Updated MetadataPlugin and MetadataProjector to replace environmentId with projectId. - Refactored ObjectQLPlugin to use projectId for scoping metadata reads/writes. - Adjusted all relevant schemas and database queries to reflect the change from env_id to project_id. - Implemented migration script to rename env_id to project_id in affected metadata tables. - Updated tests and integration points to ensure compatibility with the new projectId structure.
1 parent afa2f9b commit d4cb481

29 files changed

Lines changed: 374 additions & 205 deletions

packages/cli/src/commands/publish.ts

Lines changed: 48 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,95 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

3+
import { readFile } from 'node:fs/promises';
4+
import { resolve as resolvePath } from 'node:path';
35
import { Args, Command, Flags } from '@oclif/core';
4-
import { loadConfig } from '../utils/config.js';
56
import { printHeader, printKV, printSuccess, printError, printStep } from '../utils/format.js';
67

78
export default class Publish extends Command {
8-
static override description = 'Publish package to ObjectStack server';
9+
static override description = 'Publish a compiled artifact to ObjectStack Cloud';
910

1011
static override args = {
11-
config: Args.string({ description: 'Configuration file path', required: false }),
12+
artifact: Args.string({ description: 'Path to compiled artifact (default: dist/objectstack.json)', required: false }),
1213
};
1314

1415
static override flags = {
1516
server: Flags.string({
1617
char: 's',
17-
description: 'Server URL',
18+
description: 'ObjectStack Cloud control-plane URL',
1819
env: 'OBJECTSTACK_CLOUD_URL',
19-
default: 'http://localhost:3000',
20+
default: 'http://localhost:4000',
21+
}),
22+
project: Flags.string({
23+
char: 'p',
24+
description: 'Project ID (required)',
25+
env: 'OBJECTSTACK_PROJECT_ID',
26+
required: true,
2027
}),
2128
token: Flags.string({
2229
char: 't',
23-
description: 'Auth token',
24-
env: 'OBJECTSTACK_AUTH_TOKEN',
30+
description: 'API key for ObjectStack Cloud',
31+
env: 'OBJECTSTACK_CLOUD_API_KEY',
2532
}),
2633
};
2734

2835
async run(): Promise<void> {
2936
const { args, flags } = await this.parse(Publish);
3037

31-
printHeader('Publish Package');
38+
printHeader('Publish Artifact');
3239

3340
try {
34-
// 1. Load config
35-
printStep('Loading configuration...');
36-
const { config, absolutePath } = await loadConfig(args.config);
37-
38-
if (!config || !config.manifest) {
39-
printError('Invalid config: missing manifest');
41+
// 1. Locate the compiled artifact
42+
const artifactPath = args.artifact
43+
? resolvePath(process.cwd(), args.artifact)
44+
: resolvePath(process.cwd(), 'dist/objectstack.json');
45+
46+
printStep(`Loading artifact from ${artifactPath}...`);
47+
let artifactRaw: string;
48+
try {
49+
artifactRaw = await readFile(artifactPath, 'utf-8');
50+
} catch (err: any) {
51+
printError(`Cannot read artifact: ${err.message}. Run \`objectstack build\` first.`);
4052
this.exit(1);
53+
return;
4154
}
4255

43-
const manifest = config.manifest;
44-
45-
printSuccess(`Loaded: ${absolutePath}`);
46-
47-
// 2. Collect metadata
48-
printStep('Collecting metadata...');
49-
const metadata = {
50-
objects: config.objects || [],
51-
views: config.views || [],
52-
apps: config.apps || [],
53-
flows: config.flows || [],
54-
agents: config.agents || [],
55-
tools: config.tools || [],
56-
translations: config.translations || [],
57-
};
58-
59-
console.log('');
60-
printKV(' Package', `${manifest.id}@${manifest.version}`);
61-
printKV(' Objects', metadata.objects.length.toString());
62-
printKV(' Views', metadata.views.length.toString());
63-
printKV(' Apps', metadata.apps.length.toString());
64-
printKV(' Flows', metadata.flows.length.toString());
65-
printKV(' Agents', metadata.agents.length.toString());
66-
printKV(' Tools', metadata.tools.length.toString());
67-
printKV(' Translations', metadata.translations.length.toString());
56+
const artifact = JSON.parse(artifactRaw);
57+
printSuccess(`Loaded artifact (${(artifactRaw.length / 1024).toFixed(1)} KB)`);
6858

69-
// 3. Publish to server
70-
const serverUrl = `${flags.server}/api/v1/packages`;
59+
// 2. POST to the control-plane publish endpoint
60+
const serverUrl = `${flags.server}/api/v1/cloud/projects/${flags.project}/metadata`;
7161
printStep(`Publishing to ${serverUrl}...`);
7262

7363
const response = await fetch(serverUrl, {
7464
method: 'POST',
7565
headers: {
7666
'Content-Type': 'application/json',
77-
...(flags.token && { 'Authorization': `Bearer ${flags.token}` }),
67+
...(flags.token && { Authorization: `Bearer ${flags.token}` }),
7868
},
79-
body: JSON.stringify({ manifest, metadata }),
69+
body: artifactRaw,
8070
});
8171

8272
if (!response.ok) {
83-
const error = await response.json();
84-
printError(`Publish failed: ${error.error || response.statusText}`);
73+
let errMsg: string;
74+
try {
75+
const errBody = await response.json() as any;
76+
errMsg = errBody?.error ?? response.statusText;
77+
} catch {
78+
errMsg = response.statusText;
79+
}
80+
printError(`Publish failed (${response.status}): ${errMsg}`);
8581
this.exit(1);
82+
return;
8683
}
8784

88-
const result = await response.json();
89-
const size = (JSON.stringify(metadata).length / 1024).toFixed(2);
85+
const result = await response.json() as any;
86+
const data = result?.data ?? result;
9087

9188
console.log('');
92-
printSuccess(result.message);
93-
printKV(' Size', `${size} KB`);
89+
printSuccess('Artifact published successfully');
90+
printKV(' Project', flags.project);
91+
if (data?.commitId) printKV(' Commit', data.commitId);
92+
if (data?.checksum?.value) printKV(' Checksum', data.checksum.value.slice(0, 16));
9493
printKV(' Server', flags.server);
9594

9695
} catch (error) {

packages/metadata/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
"types": "./dist/node.d.ts",
1717
"import": "./dist/node.js",
1818
"require": "./dist/node.cjs"
19+
},
20+
"./migrations": {
21+
"types": "./dist/migrations/migrate-env-id-to-project-id.d.ts",
22+
"import": "./dist/migrations/migrate-env-id-to-project-id.js",
23+
"require": "./dist/migrations/migrate-env-id-to-project-id.cjs"
1924
}
2025
},
2126
"files": [

packages/metadata/src/loaders/database-loader.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -342,27 +342,27 @@ describe('DatabaseLoader', () => {
342342
});
343343

344344
describe('multi-tenant isolation', () => {
345-
it('should filter by organizationId and environmentId when configured', async () => {
345+
it('should filter by organizationId and projectId when configured', async () => {
346346
const tenantLoader = new DatabaseLoader({
347347
driver: mockDriver,
348348
organizationId: 'org-1',
349-
environmentId: 'env-1',
349+
projectId: 'env-1',
350350
});
351351

352352
await tenantLoader.save('object', 'account', { name: 'account' });
353353

354-
// The create call should include organization_id and env_id
354+
// The create call should include organization_id and project_id
355355
expect(mockDriver.create).toHaveBeenCalledWith(
356356
'sys_metadata',
357-
expect.objectContaining({ organization_id: 'org-1', env_id: 'env-1' })
357+
expect.objectContaining({ organization_id: 'org-1', project_id: 'env-1' })
358358
);
359359

360-
// The find calls should filter by organization_id and env_id
360+
// The find calls should filter by organization_id and project_id
361361
await tenantLoader.load('object', 'account');
362362
expect(mockDriver.findOne).toHaveBeenCalledWith(
363363
'sys_metadata',
364364
expect.objectContaining({
365-
where: expect.objectContaining({ organization_id: 'org-1', env_id: 'env-1' }),
365+
where: expect.objectContaining({ organization_id: 'org-1', project_id: 'env-1' }),
366366
})
367367
);
368368
});

packages/metadata/src/loaders/database-loader.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ export interface DatabaseLoaderOptions {
4949
/** Organization ID for multi-tenant isolation */
5050
organizationId?: string;
5151

52-
/** Environment ID — null = platform-global, set = env-scoped */
53-
environmentId?: string;
52+
/** Project ID — null = platform-global, set = project-scoped */
53+
projectId?: string;
5454

5555
/** Enable history tracking (default: true) */
5656
trackHistory?: boolean;
@@ -83,7 +83,7 @@ export class DatabaseLoader implements MetadataLoader {
8383
private tableName: string;
8484
private historyTableName: string;
8585
private organizationId?: string;
86-
private environmentId?: string;
86+
private projectId?: string;
8787
private trackHistory: boolean;
8888
private schemaReady = false;
8989
private historySchemaReady = false;
@@ -99,7 +99,7 @@ export class DatabaseLoader implements MetadataLoader {
9999
this.tableName = options.tableName ?? 'sys_metadata';
100100
this.historyTableName = options.historyTableName ?? 'sys_metadata_history';
101101
this.organizationId = options.organizationId;
102-
this.environmentId = options.environmentId;
102+
this.projectId = options.projectId;
103103
this.trackHistory = options.trackHistory !== false; // Default to true
104104
this.enableProjection = options.enableProjection !== false; // Default to true
105105

@@ -109,7 +109,7 @@ export class DatabaseLoader implements MetadataLoader {
109109
driver: this.driver,
110110
engine: this.engine,
111111
organizationId: this.organizationId,
112-
environmentId: this.environmentId,
112+
projectId: this.projectId,
113113
});
114114
}
115115
}
@@ -214,7 +214,7 @@ export class DatabaseLoader implements MetadataLoader {
214214

215215
/**
216216
* Build base filter conditions for queries.
217-
* Filters by organizationId when configured; env_id when environmentId is set,
217+
* Filters by organizationId when configured; project_id when projectId is set,
218218
* or null (platform-global) when not set.
219219
*/
220220
private baseFilter(type: string, name?: string): Record<string, unknown> {
@@ -225,8 +225,8 @@ export class DatabaseLoader implements MetadataLoader {
225225
if (this.organizationId) {
226226
filter.organization_id = this.organizationId;
227227
}
228-
// When environmentId is set, scope to that env; otherwise query platform-global (env_id = null).
229-
filter.env_id = this.environmentId ?? null;
228+
// When projectId is set, scope to that project; otherwise query platform-global (project_id = null).
229+
filter.project_id = this.projectId ?? null;
230230
return filter;
231231
}
232232

@@ -283,7 +283,7 @@ export class DatabaseLoader implements MetadataLoader {
283283
recordedBy,
284284
recordedAt: now,
285285
...(this.organizationId ? { organizationId: this.organizationId } : {}),
286-
...(this.environmentId !== undefined ? { environmentId: this.environmentId } : {}),
286+
...(this.projectId !== undefined ? { projectId: this.projectId } : {}),
287287
};
288288

289289
try {
@@ -301,7 +301,7 @@ export class DatabaseLoader implements MetadataLoader {
301301
recorded_by: historyRecord.recordedBy,
302302
recorded_at: historyRecord.recordedAt,
303303
...(this.organizationId ? { organization_id: this.organizationId } : {}),
304-
...(this.environmentId !== undefined ? { env_id: this.environmentId } : {}),
304+
...(this.projectId !== undefined ? { project_id: this.projectId } : {}),
305305
});
306306
} catch (error) {
307307
// Log error but don't fail the main operation
@@ -341,7 +341,7 @@ export class DatabaseLoader implements MetadataLoader {
341341
owner: row.owner as string | undefined,
342342
state: (row.state as MetadataRecord['state']) ?? 'active',
343343
organizationId: row.organization_id as string | undefined,
344-
environmentId: row.env_id as string | undefined,
344+
projectId: row.project_id as string | undefined,
345345
version: (row.version as number) ?? 1,
346346
checksum: row.checksum as string | undefined,
347347
source: row.source as MetadataRecord['source'],
@@ -498,7 +498,7 @@ export class DatabaseLoader implements MetadataLoader {
498498
if (this.organizationId) {
499499
filter.organization_id = this.organizationId;
500500
}
501-
filter.env_id = this.environmentId ?? null;
501+
filter.project_id = this.projectId ?? null;
502502

503503
const row = await this._findOne(this.historyTableName, {
504504
where: filter,
@@ -517,7 +517,7 @@ export class DatabaseLoader implements MetadataLoader {
517517
previousChecksum: row.previous_checksum as string | undefined,
518518
changeNote: row.change_note as string | undefined,
519519
organizationId: row.organization_id as string | undefined,
520-
environmentId: row.env_id as string | undefined,
520+
projectId: row.project_id as string | undefined,
521521
recordedBy: row.recorded_by as string | undefined,
522522
recordedAt: row.recorded_at as string,
523523
};
@@ -550,7 +550,7 @@ export class DatabaseLoader implements MetadataLoader {
550550
// Find the metadata record
551551
const filter: Record<string, unknown> = { type, name };
552552
if (this.organizationId) filter.organization_id = this.organizationId;
553-
filter.env_id = this.environmentId ?? null;
553+
filter.project_id = this.projectId ?? null;
554554

555555
const metadataRecord = await this._findOne(this.tableName, { where: filter });
556556
if (!metadataRecord) {
@@ -562,7 +562,7 @@ export class DatabaseLoader implements MetadataLoader {
562562
metadata_id: metadataRecord.id,
563563
};
564564
if (this.organizationId) historyFilter.organization_id = this.organizationId;
565-
historyFilter.env_id = this.environmentId ?? null;
565+
historyFilter.project_id = this.projectId ?? null;
566566
if (options?.operationType) historyFilter.operation_type = options.operationType;
567567
if (options?.since) historyFilter.recorded_at = { $gte: options.since };
568568
if (options?.until) {
@@ -609,7 +609,7 @@ export class DatabaseLoader implements MetadataLoader {
609609
previousChecksum: row.previous_checksum as string | undefined,
610610
changeNote: row.change_note as string | undefined,
611611
organizationId: row.organization_id as string | undefined,
612-
environmentId: row.env_id as string | undefined,
612+
projectId: row.project_id as string | undefined,
613613
recordedBy: row.recorded_by as string | undefined,
614614
recordedAt: row.recorded_at as string,
615615
};
@@ -752,7 +752,7 @@ export class DatabaseLoader implements MetadataLoader {
752752
version: 1,
753753
source: 'database',
754754
...(this.organizationId ? { organization_id: this.organizationId } : {}),
755-
...(this.environmentId !== undefined ? { env_id: this.environmentId } : { env_id: null }),
755+
...(this.projectId !== undefined ? { project_id: this.projectId } : { project_id: null }),
756756
created_at: now,
757757
updated_at: now,
758758
});

packages/metadata/src/metadata-manager.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,13 @@ export class MetadataManager implements IMetadataService {
134134
*
135135
* @param driver - An IDataDriver instance for database operations
136136
* @param organizationId - Organization ID for multi-tenant isolation
137-
* @param environmentId - Environment ID (undefined = platform-global)
137+
* @param projectId - Project ID (undefined = platform-global)
138138
*/
139-
setDatabaseDriver(driver: IDataDriver, organizationId?: string, environmentId?: string): void {
140-
if (environmentId !== undefined) {
139+
setDatabaseDriver(driver: IDataDriver, organizationId?: string, projectId?: string): void {
140+
if (projectId !== undefined) {
141141
this.logger.info('Project kernel — skipping DatabaseLoader for sys_metadata (control-plane only)', {
142142
organizationId,
143-
environmentId,
143+
projectId,
144144
});
145145
return;
146146
}
@@ -149,7 +149,7 @@ export class MetadataManager implements IMetadataService {
149149
driver,
150150
tableName,
151151
organizationId,
152-
environmentId,
152+
projectId,
153153
});
154154
this.registerLoader(dbLoader);
155155
this.logger.info('DatabaseLoader configured', { datasource: this.config.datasource, tableName });
@@ -163,13 +163,13 @@ export class MetadataManager implements IMetadataService {
163163
*
164164
* @param engine - An IDataEngine instance (typically the ObjectQL service)
165165
* @param organizationId - Organization ID for multi-tenant isolation
166-
* @param environmentId - Environment ID (undefined = platform-global)
166+
* @param projectId - Project ID (undefined = platform-global)
167167
*/
168-
setDataEngine(engine: IDataEngine, organizationId?: string, environmentId?: string): void {
169-
if (environmentId !== undefined) {
168+
setDataEngine(engine: IDataEngine, organizationId?: string, projectId?: string): void {
169+
if (projectId !== undefined) {
170170
this.logger.info('Project kernel — skipping DatabaseLoader for sys_metadata (control-plane only)', {
171171
organizationId,
172-
environmentId,
172+
projectId,
173173
});
174174
return;
175175
}
@@ -178,7 +178,7 @@ export class MetadataManager implements IMetadataService {
178178
engine,
179179
tableName,
180180
organizationId,
181-
environmentId,
181+
projectId,
182182
});
183183
this.registerLoader(dbLoader);
184184
this.logger.info('DatabaseLoader configured via DataEngine', { tableName });

0 commit comments

Comments
 (0)