-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Package-level unified metadata publishing mechanism (publishPackage) #825
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4f66710
6bef205
1af3247
8741749
d62fbe4
1b502b5
b7b8c3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,6 +15,7 @@ import type { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MetadataSaveResult, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MetadataWatchEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MetadataFormat, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PackagePublishResult, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '@objectstack/spec/system'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IMetadataService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -333,6 +334,187 @@ export class MetadataManager implements IMetadataService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Publish an entire package: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 1. Validate all draft items | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 2. Snapshot all items in the package (publishedDefinition = clone(metadata)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 3. Increment version | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 4. Set all items state → active | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async publishPackage(packageId: string, options?: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| changeNote?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| publishedBy?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validate?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }): Promise<PackagePublishResult> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const now = new Date().toISOString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const shouldValidate = options?.validate !== false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const publishedBy = options?.publishedBy; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+344
to
+352
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Collect all items belonging to this package | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const packageItems: Array<{ type: string; name: string; data: any }> = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [type, typeStore] of this.registry) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [name, data] of typeStore) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const meta = data as any; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (meta?.packageId === packageId || meta?.package === packageId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| packageItems.push({ type, name, data: meta }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+354
to
+360
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const packageItems: Array<{ type: string; name: string; data: any }> = []; | |
| for (const [type, typeStore] of this.registry) { | |
| for (const [name, data] of typeStore) { | |
| const meta = data as any; | |
| if (meta?.packageId === packageId || meta?.package === packageId) { | |
| packageItems.push({ type, name, data: meta }); | |
| } | |
| const packageItems: Array<{ type: string; name: string; data: any }> = []; | |
| const seenKeys = new Set<string>(); | |
| // 1) In-memory registry scan (existing behavior) | |
| for (const [type, typeStore] of this.registry) { | |
| for (const [name, data] of typeStore) { | |
| const meta = data as any; | |
| if (meta?.packageId === packageId || meta?.package === packageId) { | |
| const key = `${type}:${name}`; | |
| if (!seenKeys.has(key)) { | |
| seenKeys.add(key); | |
| packageItems.push({ type, name, data: meta }); | |
| } | |
| } | |
| } | |
| } | |
| // 2) Loader-backed collection via query API (ensures persisted-only items are included) | |
| // We access `query` through `any` to avoid changing the public interface here while still | |
| // leveraging the loader-backed implementation that likely exists behind IMetadataService. | |
| if (typeof (this as any).query === 'function') { | |
| try { | |
| const queryResult = await (this as any).query({ packageId } as any); | |
| const rawItems: any[] = | |
| (queryResult && (queryResult.items ?? queryResult.results ?? queryResult.data)) ?? []; | |
| for (const raw of rawItems) { | |
| const meta = raw as any; | |
| const itemPackageId = meta?.packageId ?? meta?.package; | |
| if (itemPackageId !== packageId) continue; | |
| const type: string | undefined = | |
| meta?.type ?? meta?.metadataType ?? meta?.kind; | |
| const name: string | undefined = | |
| meta?.name ?? meta?.id; | |
| if (!type || !name) continue; | |
| const key = `${type}:${name}`; | |
| if (seenKeys.has(key)) continue; | |
| seenKeys.add(key); | |
| packageItems.push({ type, name, data: meta }); | |
| } | |
| } catch (err) { | |
| // Fallback silently to registry-only behavior if the query API fails; | |
| // publishing should not crash due to an unsupported/failed query call. | |
| if ((this as any).logger && typeof (this as any).logger.warn === 'function') { | |
| (this as any).logger.warn( | |
| `publishPackage: loader-backed query failed for package '${packageId}':`, | |
| err, | |
| ); | |
| } |
Copilot
AI
Feb 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code uses the existing version field to represent the package publish version (newVersion) and overwrites it for every item. In this codebase, MetadataRecord.version is already used for optimistic concurrency / update counting (e.g. DatabaseLoader increments it on each save). Mixing these semantics will make publish versions drift with edits and can break concurrency expectations. Consider introducing a dedicated publish/version field (e.g. publishedVersion/packageVersion) and keep version for concurrency, or compute publish version from a separate stored counter.
Copilot
AI
Feb 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
publishedDefinition is set via structuredClone(item.data.metadata ?? item.data). If an item is missing the metadata payload envelope, this snapshots the entire record object (including audit fields / previously publishedDefinition), and revertPackage() later writes that back into metadata, corrupting the stored shape. It would be safer to snapshot only item.data.metadata (and treat missing metadata as a validation error) so publishedDefinition consistently represents the published definition payload.
Copilot
AI
Feb 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
revertPackage() only updates items that already have publishedDefinition. New items created after the last publish (no publishedDefinition) are left untouched, so a “revert” can still leave unpublished additions in the package. Consider removing/archiving items without a published snapshot when reverting, so the package truly returns to the last published state.
| await this.register(item.type, item.name, reverted); | |
| await this.register(item.type, item.name, reverted); | |
| } else { | |
| // Item was created after the last publish and has no published snapshot. | |
| // Remove it so the package truly matches the last published state. | |
| const typeStore = this.registry.get(item.type); | |
| typeStore?.delete(item.name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docs list REST endpoints under
/metadata/..., but the runtime/adapters in this repo expose metadata routes under/meta(e.g./api/v1/meta/...). To avoid sending users to non-existent endpoints, update these paths to the actual/meta/:type/:name/publishedroute (and keep it consistent with the rest of the docs).