From 4f8d8f22cc61da830b763f621b5a5099924ae199 Mon Sep 17 00:00:00 2001 From: Gasser Date: Fri, 7 Nov 2025 12:14:09 +0100 Subject: [PATCH 01/12] wip --- .../aws-service-spec/build/full-database.ts | 10 +- .../schemas/EventBridge.schema.json | 97 ++++ .../src/cli/import-db.ts | 1 + .../service-spec-importers/src/db-builder.ts | 27 + .../src/event-builder.ts | 509 ++++++++++++++++++ .../importers/import-eventbridge-schema.ts | 426 +++++++++++++++ .../src/loaders/load-eventbridge-schema.ts | 71 +++ .../types/eventbridge/EventBridgeSchema.ts | 18 + .../service-spec-importers/src/types/index.ts | 1 + .../@aws-cdk/service-spec-types/src/index.ts | 1 + .../service-spec-types/src/types/database.ts | 6 + .../service-spec-types/src/types/event.ts | 21 + 12 files changed, 1184 insertions(+), 4 deletions(-) create mode 100644 packages/@aws-cdk/service-spec-importers/schemas/EventBridge.schema.json create mode 100644 packages/@aws-cdk/service-spec-importers/src/event-builder.ts create mode 100644 packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts create mode 100644 packages/@aws-cdk/service-spec-importers/src/loaders/load-eventbridge-schema.ts create mode 100644 packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts create mode 100644 packages/@aws-cdk/service-spec-types/src/types/event.ts diff --git a/packages/@aws-cdk/aws-service-spec/build/full-database.ts b/packages/@aws-cdk/aws-service-spec/build/full-database.ts index ab832c1ba..5848b2abb 100644 --- a/packages/@aws-cdk/aws-service-spec/build/full-database.ts +++ b/packages/@aws-cdk/aws-service-spec/build/full-database.ts @@ -1,10 +1,10 @@ import * as path from 'node:path'; -import { SpecDatabase } from '@aws-cdk/service-spec-types'; import { DatabaseBuilder, DatabaseBuilderOptions, ReportAudience } from '@aws-cdk/service-spec-importers'; +import { SpecDatabase } from '@aws-cdk/service-spec-types'; import { Augmentations } from './augmentations'; -import { Scrutinies } from './scrutinies'; -import { patchSamTemplateSpec } from './patches/sam-patches'; import { patchCloudFormationRegistry } from './patches/registry-patches'; +import { patchSamTemplateSpec } from './patches/sam-patches'; +import { Scrutinies } from './scrutinies'; const SOURCES = path.join(__dirname, '../../../../sources'); @@ -27,7 +27,9 @@ export class FullDatabase extends DatabaseBuilder { path.join(SOURCES, 'CloudWatchConsoleServiceDirectory/CloudWatchConsoleServiceDirectory.json'), ) .importScrutinies() - .importAugmentations(); + .importAugmentations() + .importEventBridgeSchema(path.join(SOURCES, 'EventBridgeSchema')); + // TODO: Add patch options for the resources decider } /** diff --git a/packages/@aws-cdk/service-spec-importers/schemas/EventBridge.schema.json b/packages/@aws-cdk/service-spec-importers/schemas/EventBridge.schema.json new file mode 100644 index 000000000..69bdbc881 --- /dev/null +++ b/packages/@aws-cdk/service-spec-importers/schemas/EventBridge.schema.json @@ -0,0 +1,97 @@ +{ + "$ref": "#/definitions/EventBridgeSchema", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "EventBridgeSchema": { + "additionalProperties": false, + "description": "Root class of an EventBridge Schema", + "properties": { + "SchemaName": { + "type": "string" + }, + "Content": { + "$ref": "#/definitions/EventBridgeContent" + }, + "Description": { + "type": "string" + }, + "Type": { + "type": "string" + } + }, + "required": [ + "SchemaName", + "Content" + ], + "type": "object" + }, + "EventBridgeContent": { + "additionalProperties": false, + "properties": { + "components": { + "$ref": "#/definitions/EventBridgeComponents" + } + }, + "required": [ + "components" + ], + "type": "object" + }, + "EventBridgeComponents": { + "additionalProperties": false, + "properties": { + "schemas": { + "$ref": "#/definitions/EventBridgeSchemas" + } + }, + "required": [ + "schemas" + ], + "type": "object" + }, + "EventBridgeSchemas": { + "additionalProperties": false, + "properties": { + "AWSEvent": { + "$ref": "#/definitions/AWSEvent" + } + }, + "required": [ + "AWSEvent" + ], + "type": "object" + }, + "AWSEvent": { + "additionalProperties": false, + "properties": { + "x-amazon-events-detail-type": { + "type": "string" + }, + "x-amazon-events-source": { + "type": "string" + }, + "properties": { + "$ref": "#/definitions/AWSEventProperties" + } + }, + "required": [ + "x-amazon-events-detail-type", + "x-amazon-events-source", + "properties" + ], + "type": "object" + }, + "AWSEventProperties": { + "additionalProperties": false, + "properties": { + "detail": { + "type": "object" + } + }, + "required": [ + "detail" + ], + "type": "object" + } + } +} diff --git a/packages/@aws-cdk/service-spec-importers/src/cli/import-db.ts b/packages/@aws-cdk/service-spec-importers/src/cli/import-db.ts index 658d7236a..72c858a38 100644 --- a/packages/@aws-cdk/service-spec-importers/src/cli/import-db.ts +++ b/packages/@aws-cdk/service-spec-importers/src/cli/import-db.ts @@ -16,6 +16,7 @@ const AVAILABLE_SOURCES: Record = { cannedMetrics: 'importCannedMetrics', arnTemplates: 'importArnTemplates', oobRelationships: 'importOobRelationships', + eventbridgeschema: 'importEventBridgeSchema', }; async function main() { diff --git a/packages/@aws-cdk/service-spec-importers/src/db-builder.ts b/packages/@aws-cdk/service-spec-importers/src/db-builder.ts index 24e97297f..5fd03fc6c 100644 --- a/packages/@aws-cdk/service-spec-importers/src/db-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/db-builder.ts @@ -5,6 +5,7 @@ import { importArnTemplates } from './importers/import-arn-templates'; import { importCannedMetrics } from './importers/import-canned-metrics'; import { importCloudFormationDocumentation } from './importers/import-cloudformation-docs'; import { importCloudFormationRegistryResource } from './importers/import-cloudformation-registry'; +import { importEventBridgeSchema } from './importers/import-eventbridge-schema'; import { importGetAttAllowList } from './importers/import-getatt-allowlist'; import { importOobRelationships } from './importers/import-oob-relationships'; import { ResourceSpecImporter, SAMSpecImporter } from './importers/import-resource-spec'; @@ -22,6 +23,7 @@ import { loadSamSpec, loadOobRelationships, } from './loaders'; +import { loadDefaultEventBridgeSchema } from './loaders/load-eventbridge-schema'; import { JsonLensPatcher } from './patching'; import { ProblemReport, ReportAudience } from './report'; @@ -209,6 +211,31 @@ export class DatabaseBuilder { }); } + /** + * Import the EvnetBridge schema + */ + public importEventBridgeSchema(schemaDirectory: string) { + return this.addSourceImporter(async (db, report) => { + const regions = await loadDefaultEventBridgeSchema(schemaDirectory, { + ...this.options, + report, + failureAudience: this.defaultProblemGrouping, + // patcher, + }); + for (const region of regions) { + for (const event of region.events) { + console.log({ region, resource: JSON.stringify(event, null, 2), name: event.SchemaName }); + importEventBridgeSchema({ + db, + event, + report, + region: region.regionName, + }); + } + } + }); + } + /** * Look at a load result and report problems */ diff --git a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts new file mode 100644 index 000000000..236f8d193 --- /dev/null +++ b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts @@ -0,0 +1,509 @@ +import { Service, SpecDatabase, Event, EventProperties } from '@aws-cdk/service-spec-types'; +import { Entity, Reference } from '@cdklabs/tskb'; + +// TODO: those aren't optional put them somehow required +export interface EventBuilderOptions { + readonly source: string; + readonly detailType: string; +} + +// export interface Event extends Entity { +// readonly name: string; +// readonly source: string; +// readonly detailType: string; +// readonly identifiersPath: Array; +// readonly properties: Record>; +// } +// +// export type HasEvent = Relationship; + +export class SpecBuilder { + constructor(public readonly db: SpecDatabase) {} + + public eventBuilder(schemaName: string, options: EventBuilderOptions) { + const existing = this.db.lookup('event', 'name', 'equals', schemaName); + + if (existing.length > 0) { + return; + // FIX: IMP when there's no service need to return something + const event = existing.only(); + // if (!event.documentation && options.description) { + // event.documentation = options.description; + // } + // if (!event.primaryIdentifier) { + // event.primaryIdentifier = options.primaryIdentifier; + // } + + return new EventBuilder(this.db, event); + } + + // FIX: mocking a type just for not blocking the code generation, need to be removed + + const typeDef = this.db.allocate('typeDefinition', { + name: 'mockTypeName', + properties: { + mockFieldName: { + type: { + type: 'string', + }, + }, + }, + }); + + function ref(x: E | string): Reference { + return typeof x === 'string' ? { $ref: x } : { $ref: x.$id }; + } + + const typeDef2 = this.db.allocate('typeDefinition', { + name: 'mockTypeName2', + properties: { + mockFieldName: { + type: { + type: 'ref', + reference: ref(typeDef), + }, + }, + }, + }); + typeDef.name; + const event = this.db.allocate('event', { + // FIX: need to fix the bang? + name: schemaName.split('@').pop()!, + source: options.source, + detailType: options.detailType, + properties: { + mockTypeName2: { + type: { + type: 'ref', + reference: ref(typeDef2), + }, + }, + }, + identifiersPath: ['mockTypeName2.mockTypeName'], + // attributes: {}, + }); + + this.db.link('eventUsesType', event, typeDef); + this.db.link('eventUsesType', event, typeDef2); + // mocking type ends + // + // TODO: add more information for the event + + const service = this.allocateService(schemaName); + if (service == undefined) { + // TODO: Maybe i need to return undefined + return new EventBuilder(this.db, event); + } + const resource = this.allocateResource(service); + console.log('hasEvent link is creating...'); + console.log({ resource: JSON.stringify(resource), event: JSON.stringify(event) }); + // TODO: should i return the entity only + this.db.link('hasEvent', resource.entity, event); + // TODO: Do i need to do this?? + // if (options.region) { + // const region = this.allocateRegion(options.region); + // this.db.link('regionHasResource', region, resource); + // this.db.link('regionHasService', region, service); + // } + + return new EventBuilder(this.db, event); + } + + // TODO: change name to get? + private allocateService(eventSchemaName: string, eventTypeNameSeparator = '@') { + const parts = eventSchemaName.split(eventTypeNameSeparator); + // parts e.g. ["aws.s3", "ObjectCreated"] + const serviceName = parts[0].replace('.', '-').toLowerCase(); + + const services = this.db.lookup('service', 'name', 'equals', serviceName); + + if (services.length == 0) { + return; + } + + // TODO: i think only will do that for me + // if (true) { + // throw Error(`This service doesn't existing in cloudformation ${serviceName}`); + // } + return services.only(); + } + + // TODO: change name to get? + private allocateResource(service: Service) { + const resource = this.resourceDecider(service); + + return resource; + // TODO: I have no idea what i'm doing now :D, how the resource will not be in the DB? + // const resource = this.db.allocate('service', { + // name, + // shortName, + // capitalized, + // cloudFormationNamespace, + // }); + + // return resource; + } + + private resourceDecider(service: Service) { + // TODO: need to get all the requred property field names here + const resources = this.db.follow('hasResource', service); + if (service.name == 'aws-lambda') { + console.log({ resources: JSON.stringify(resources, null, 2) }); + } + + // TODO: Change this to proper resource + return resources[0]; + } +} +// +interface ObjectWithProperties { + properties: EventProperties; +} + +export class PropertyBagBuilder { + // protected candidateProperties: ResourceProperties = {}; + // + // @ts-ignore + constructor(private readonly _propertyBag: ObjectWithProperties) {} + // + // public setProperty(name: string, prop: Property) { + // this.candidateProperties[name] = prop; + // } + // + // /** + // * Delete a property from the builder + // * + // * This avoids committing it to the underlying property bag -- if the underlying + // * bag already has the property, it will not be removed. + // */ + // public unsetProperty(name: string) { + // delete this.candidateProperties[name]; + // } + // + // /** + // * Commit the property and attribute changes to the underlying property bag. + // */ + // public commit(): ObjectWithProperties { + // for (const [name, prop] of Object.entries(this.candidateProperties)) { + // this.commitProperty(name, prop); + // } + // + // return this._propertyBag; + // } + // + // private commitProperty(name: string, prop: Property) { + // if (this._propertyBag.properties[name]) { + // this.mergeProperty(this._propertyBag.properties[name], prop); + // } else { + // this._propertyBag.properties[name] = prop; + // } + // this.simplifyProperty(this._propertyBag.properties[name]); + // } + // + // protected mergeProperty(prop: Property, updates: Property) { + // // This handles merges that are trivial scalar overwrites. All + // // fields must be represented, if you have special code to handle + // // a field, put it in here as 'undefined' and add code to handle it below. + // copyDefined({ + // causesReplacement: updates.causesReplacement, + // defaultValue: updates.defaultValue, + // deprecated: updates.deprecated, + // documentation: updates.documentation, + // required: updates.required, + // scrutinizable: updates.scrutinizable, + // relationshipRefs: updates.relationshipRefs, + // + // // These will be handled specially below + // previousTypes: undefined, + // type: undefined, + // }); + // + // // Special field handling + // for (const type of updates.previousTypes ?? []) { + // new RichProperty(prop).updateType(type); + // } + // new RichProperty(prop).updateType(updates.type); + // + // function copyDefined(upds: AllFieldsGiven>) { + // for (const [key, value] of Object.entries(upds)) { + // if (value !== undefined) { + // (prop as any)[key] = value; + // } + // } + // } + // } + // + // /** + // * Remove settings that are equal to their defaults + // */ + // protected simplifyProperty(prop: Property) { + // if (!prop.required) { + // delete prop.required; + // } + // if (prop.causesReplacement === 'no') { + // delete prop.causesReplacement; + // } + // } +} + +export class EventBuilder extends PropertyBagBuilder { + // // private typeDefinitions = new Map(); + // // private typesCreatedHere = new Set(); + // // + // // /** + // // * Keep a copy of all properties configured here + // // * + // // * We'll need some of them later to turn them into attributes. + // // */ + // // private allProperties: ResourceProperties = {}; + // // + // // private candidateAttributes: ResourceProperties = {}; + // // + + // @ts-ignore + constructor(public readonly db: SpecDatabase, private readonly event: Event) { + super(event); + // this.indexExistingTypeDefinitions(); + } + // // + // // public get cloudFormationType(): string { + // // return this.resource.cloudFormationType; + // // } + // // + // // public setProperty(name: string, prop: Property) { + // // super.setProperty(name, prop); + // // this.allProperties[name] = prop; + // // } + // // + // // public setAttribute(name: string, attr: Attribute) { + // // this.candidateAttributes[name] = attr; + // // } + // // + // // public unsetAttribute(name: string) { + // // delete this.candidateAttributes[name]; + // // } + // // + // // /** + // // * Mark the given properties as attributes instead + // // * + // // * These can be simple property names (`Foo`, `Bar`), but they can also be + // // * compound property names (`Foo/Bar`), and the compound property names can + // // * contain array wildcards (`Foo/*­/Bar`). + // // * + // // * In the CloudFormation resource spec, compound property names are separated + // // * by periods (`Foo.Bar`). + // // * + // // * In upconverted CloudFormation resource specs -> registry specs, the compound + // // * property name references may contain a period, while the actual property name + // // * in the properties bag has the periods stripped: attributeName is `Foo.Bar`, + // // * but the actual property name is `FooBar`. + // // * + // // * The same deep property name may occur multiple times (`Foo`, `Foo/Bar`, `Foo/Baz`). + // // */ + // // public markAsAttributes(props: string[]) { + // // for (const propName of props) { + // // if (this.candidateProperties[propName]) { + // // this.setAttribute(propName, this.candidateProperties[propName]); + // // this.unsetProperty(propName); + // // continue; + // // } + // // + // // // In case of a half-upconverted legacy spec, the property might also + // // // exist with a name that has any `.` stripped. + // // const strippedName = stripPeriods(propName); + // // if (this.candidateProperties[strippedName]) { + // // // The ACTUAL name is still the name with '.' in it, but we copy the type + // // // from the stripped name. + // // this.setAttribute(propName, this.candidateProperties[strippedName]); + // // this.unsetProperty(strippedName); + // // continue; + // // } + // // + // // // Otherwise assume the name represents a compound attribute + // // // In the Registry spec, compound attributes will look like 'Container/Prop'. + // // // In the legacy spec they will look like 'Container.Prop'. + // // // Some Registry resources incorrectly use '.' as well. + // // // We accept both here, but turn them both into '.'-separated. + // // // + // // // Sometimes this contains a `*`, to indicate that it could be any element in an array. + // // // We can't currently support those, so we drop them (ex: `Subscribers/*/Status`). + // // // + // // // We don't remove the top-level properties from the resource, we just add the attributes. + // // const propPath = propName.split(/[\.\/]/); + // // const propWithPeriods = propPath.join('.'); + // // if (propPath.includes('*')) { + // // // Skip unrepresentable + // // continue; + // // } + // // + // // try { + // // const prop = this.propertyDeep(...propPath); + // // if (prop) { + // // this.setAttribute(propWithPeriods, prop); + // // + // // // FIXME: not sure if we need to delete property `Foo` if the only + // // // attribute reference we got is `Foo/Bar`. Let's not for now. + // // } + // // } catch (e: any) { + // // // We're catching any errors from propertyDeep because CloudFormation allows schemas + // // // where attribute properties are not part of the spec anywhere else. Although it is + // // // likely a bad schema, CDK forges ahead by just dropping the attribute. + // // // Example: `ProviderDetails` typed as `Map` and `"readOnlyProperties: ['/properties/ProviderDetails/Attribute']"` + // // console.log(`Attribute cannot be found in the spec. Error returned: ${e}.`); + // // } + // // } + // // } + // // + // // /** + // // * Mark the given properties as immutable + // // * + // // * This be a top-level property reference, or a deep property reference, like `Foo` or + // // * `Foo/Bar`. + // // */ + // // public markAsImmutable(props: string[]) { + // // for (const propName of props) { + // // const propPath = propName.split(/\//); + // // + // // try { + // // const prop = this.propertyDeep(...propPath); + // // if (prop) { + // // prop.causesReplacement = 'yes'; + // // } + // // } catch { + // // if (!this.resource.additionalReplacementProperties) { + // // this.resource.additionalReplacementProperties = []; + // // } + // // this.resource.additionalReplacementProperties.push(propPath); + // // } + // // } + // // } + // // + // // public markDeprecatedProperties(...props: string[]) { + // // for (const propName of props) { + // // (this.candidateProperties[propName] ?? {}).deprecated = Deprecation.WARN; + // // } + // // } + // // + // // public setTagInformation(tagInfo: TagInformation) { + // // this.resource.tagInformation = tagInfo; + // // } + // // + // // public propertyDeep(...fieldPath: string[]): Property | undefined { + // // // The property bag we're searching in. Start by searching 'allProperties', not + // // // the current set of resource props (as markAsAttributes may have deleted some of them) + // // let currentBag: ResourceProperties = this.allProperties; + // // + // // for (let i = 0; i < fieldPath.length - 1; i++) { + // // const prop = currentBag[fieldPath[i]]; + // // if (!prop) { + // // throw new Error( + // // `${this.resource.cloudFormationType}: no definition for property: ${fieldPath.slice(0, i + 1).join('/')}`, + // // ); + // // } + // // + // // let propType = prop.type; + // // + // // // Handle '*'s + // // while (fieldPath[i + 1] === '*') { + // // if (propType.type !== 'array' && propType.type !== 'map') { + // // throw new Error( + // // `${this.resource.cloudFormationType}: ${fieldPath.join('/')}: expected array but ${fieldPath + // // .slice(0, i + 1) + // // .join('/')} is a ${new RichPropertyType(propType).stringify(this.db)}`, + // // ); + // // } + // // + // // propType = propType.element; + // // i += 1; + // // } + // // + // // if (propType.type !== 'ref') { + // // throw new Error( + // // `${this.resource.cloudFormationType}: ${fieldPath.join('/')}: expected type definition but ${fieldPath + // // .slice(0, i + 1) + // // .join('/')} is a ${new RichPropertyType(propType).stringify(this.db)}`, + // // ); + // // } + // // + // // const typeDef = this.db.get('typeDefinition', propType.reference); + // // currentBag = typeDef.properties; + // // } + // // + // // return currentBag[fieldPath[fieldPath.length - 1]]; + // // } + // // + // // public typeDefinitionBuilder( + // // typeName: string, + // // options?: { description?: string; schema?: jsonschema.RecordLikeObject }, + // // ) { + // // const existing = this.typeDefinitions.get(typeName); + // // const description = options?.description; + // // const freshInSession = !this.typesCreatedHere.has(typeName); + // // this.typesCreatedHere.add(typeName); + // // + // // if (existing) { + // // if (!existing.documentation && description) { + // // existing.documentation = description; + // // } + // // const properties = options?.schema?.properties ?? {}; + // // // If db already contains typeName's type definition, we want to additionally + // // // check if the schema matches the type definition. If the schema includes new + // // // properties, we want to add them to the type definition. + // // if (!Object.keys(properties).every((element) => Object.keys(existing.properties).includes(element))) { + // // return { + // // typeDefinitionBuilder: new TypeDefinitionBuilder(this.db, existing), + // // freshInDb: true, + // // freshInSession: true, + // // }; + // // } + // // return { + // // typeDefinitionBuilder: new TypeDefinitionBuilder(this.db, existing), + // // freshInDb: false, + // // freshInSession, + // // }; + // // } + // // + // // const typeDef = this.db.allocate('typeDefinition', { + // // name: typeName, + // // documentation: description, + // // properties: {}, + // // }); + // // this.db.link('usesType', this.resource, typeDef); + // // this.typeDefinitions.set(typeName, typeDef); + // // + // // const builder = new TypeDefinitionBuilder(this.db, typeDef); + // // return { typeDefinitionBuilder: builder, freshInDb: true, freshInSession }; + // // } + // // + // // /** + // // * Commit the property and attribute changes to the resource. + // // */ + // // public commit(): Resource { + // // // Commit properties + // // super.commit(); + // // + // // for (const [name, attr] of Object.entries(this.candidateAttributes)) { + // // this.commitAttribute(name, attr); + // // } + // // + // // return this.resource; + // // } + // // + // // private commitAttribute(name: string, attr: Attribute) { + // // if (this.resource.attributes[name]) { + // // this.mergeProperty(this.resource.attributes[name], attr); + // // } else { + // // this.resource.attributes[name] = attr; + // // } + // // this.simplifyProperty(this.resource.attributes[name]); + // // } + // // + // // /** + // // * Index the existing type definitions currently in the DB + // // */ + // // private indexExistingTypeDefinitions() { + // // for (const { entity: typeDef } of this.db.follow('usesType', this.resource)) { + // // this.typeDefinitions.set(typeDef.name, typeDef); + // // } + // // } +} diff --git a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts new file mode 100644 index 000000000..fdeffc11b --- /dev/null +++ b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts @@ -0,0 +1,426 @@ +import { SpecDatabase } from '@aws-cdk/service-spec-types'; +import { failure } from '@cdklabs/tskb'; +import { SpecBuilder } from '../event-builder'; +import { ProblemReport, ReportAudience } from '../report'; +import { EventBridgeSchema } from '../types'; + +export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) { + const { db, event } = options; + // FIX: this pointing toward CF resource + // @ts-ignore + const report = options.report.forAudience(ReportAudience.fromCloudFormationResource(event.SchemaName)); + + const specBuilder = new SpecBuilder(db); + // @ts-ignore + const eventBuilder = specBuilder.eventBuilder(event.SchemaName, { + source: event.Content.components.schemas.AWSEvent['x-amazon-events-source'], + detailType: event.Content.components.schemas.AWSEvent['x-amazon-events-detail-type'], + }); + // @ts-ignore + const eventFailure = failure.in(event.SchemaName); + + // recurseProperties(event, eventBuilder, eventFailure); + // + // // AWS::CloudFront::ContinuousDeploymentPolicy recently introduced a change where they're marking deprecatedProperties + // // as `/definitions//properties/` instead of `/properties///`. Ignore those, as it's + // // out-of-spec + // const deprecatedProperties = (event.deprecatedProperties ?? []) + // .filter((p) => p.startsWith('/properties/')) + // .map(simplePropNameFromJsonPtr); + // eventBuilder.markDeprecatedProperties(...deprecatedProperties); + // + // // Mark everything 'readOnlyProperties` as attributes. This removes them from 'properties', as + // // in the CloudFormation registry spec, fields cannot be attributes and properties at the same time. + // const attributeNames = findAttributes(event).map(simplePropNameFromJsonPtr); + // eventBuilder.markAsAttributes(attributeNames); + // + // // Mark all 'createOnlyProperties' as immutable. + // eventBuilder.markAsImmutable((event.createOnlyProperties ?? []).map(simplePropNameFromJsonPtr)); + // + // handleFailure(handleTags(eventFailure)); + // return eventBuilder.commit(); + // + // // FIX: jsonschema pointing toward cloudformation thing + // const resolve = jsonschema.makeResolver(event); + // function recurseProperties(source: ImplicitJsonSchemaRecord, target: PropertyBagBuilder, fail: Fail) { + // if (!source.properties) { + // throw new Error(`Not an object type with properties: ${JSON.stringify(source)}`); + // } + // + // const required = calculateDefinitelyRequired(source); + // + // for (const [name, property] of Object.entries(source.properties)) { + // try { + // let resolvedSchema = resolve(property); + // const relationships = collectPossibleRelationships(resolvedSchema); + // withResult(schemaTypeToModelType(name, resolvedSchema, fail.in(`property ${name}`)), (type) => { + // target.setProperty(name, { + // type, + // documentation: descriptionOf(resolvedSchema), + // required: required.has(name), + // defaultValue: describeDefault(resolvedSchema), + // relationshipRefs: relationships.length > 0 ? relationships : undefined, + // }); + // }); + // } catch (e) { + // report.reportFailure( + // 'interpreting', + // fail(`Skip generating property ${name} for resource ${event.typeName} because of ${e}`), + // ); + // } + // } + // } + // + // /** + // * Recursively extracts relationshipRef metadata from JSON schemas + // * + // * Finds relationshipRef objects that indicate this property references + // * another CloudFormation resource, and converts property paths to name. + // * This function handles the following cases: + // * Single relationship for property -> { type: 'string', relationshipRef: {...} } + // * When there are multiple relationships or the relationships are nested behind + // * layers of oneOf/anyOf/allOf the data looks like + // * { type: 'string', anyOf: [ { relationshipRef: {...} }, ...]} + // */ + // function collectPossibleRelationships(schema: jsonschema.ConcreteSchema): RelationshipRef[] { + // if (jsonschema.isAnyType(schema)) { + // return []; + // } + // + // if (jsonschema.isCombining(schema)) { + // return jsonschema.innerSchemas(schema).flatMap((s) => collectPossibleRelationships(s)); + // } else if (jsonschema.isArray(schema) && schema.items) { + // return collectPossibleRelationships(resolve(schema.items)); + // } else if ('relationshipRef' in schema && jsonschema.isRelationshipRef(schema.relationshipRef)) { + // return [ + // { + // cloudFormationType: schema.relationshipRef.typeName, + // propertyName: simplePropNameFromJsonPtr(schema.relationshipRef.propertyPath), + // }, + // ]; + // } + // return []; + // } + // + // /** + // * Convert a JSON schema type to a type in the database model + // */ + // function schemaTypeToModelType( + // propertyName: string, + // resolvedSchema: jsonschema.ResolvedSchema, + // fail: Fail, + // ): Result { + // return tryCatch(fail, (): Result => { + // const reference = jsonschema.resolvedReference(resolvedSchema); + // const referenceName = jsonschema.resolvedReferenceName(resolvedSchema); + // const nameHint = referenceName ? lastWord(referenceName) : lastWord(propertyName); + // + // if (jsonschema.isAnyType(resolvedSchema)) { + // return { type: 'json' }; + // } else if (jsonschema.isOneOf(resolvedSchema) || jsonschema.isAnyOf(resolvedSchema)) { + // const inner = jsonschema.innerSchemas(resolvedSchema); + // // The union type is a type definition + // // This is something we don't support at the moment as it's effectively a XOR for the property type + // // In future we should create an enum like class for this, but we cannot at the moment to maintain backwards compatibility + // // For now we assume the union is merged into a single object + // if (reference && inner.every((s) => jsonschema.isObject(s))) { + // report.reportFailure( + // 'interpreting', + // fail(`Ref ${referenceName} is a union of objects. Merging into a single type.`), + // ); + // const combinedType = unionSchemas(...inner) as jsonschema.ConcreteSchema; + // if (isFailure(combinedType)) { + // return combinedType; + // } + // return schemaTypeToModelType(nameHint, jsonschema.setResolvedReference(combinedType, reference), fail); + // } + // + // // Validate oneOf and anyOf types schema by validating whether there are two definitions in oneOf/anyOf + // // that has the same property name but different types. For simplicity, we do not validate if the types + // // are overlapping. We will add this case to the problem report. An sample schema would be i.e. + // // foo: { oneOf: [ { properties: { type: ObjectA } }, { properties: { type: ObjectB } }]} + // validateCombiningSchemaType(inner, fail); + // + // const convertedTypes = inner.map((t) => { + // if (jsonschema.isObject(t) && jsonschema.isRecordLikeObject(t)) { + // // The item in union type is an object with properties + // // We need to remove 'required' constraint from the object schema definition as we're dealing + // // with oneOf/anyOf. Note that we should ONLY remove 'required' when the 'required' constraint + // // refers to the object itself not the inner properties + // const refName = jsonschema.resolvedReferenceName(t); + // if ((t.title && t.required?.includes(t.title)) || (refName && t.required?.includes(refName))) { + // report.reportFailure( + // 'interpreting', + // fail( + // `${propertyName} is a union of objects. Merging into a single type and removing required fields for oneOf and anyOf.`, + // ), + // ); + // return schemaTypeToModelType(nameHint, resolve({ ...t, required: undefined }), fail); + // } + // } + // return schemaTypeToModelType(nameHint, resolve(t), fail); + // }); + // report.reportFailure('interpreting', ...convertedTypes.filter(isFailure)); + // + // const types = convertedTypes.filter(isSuccess); + // removeUnionDuplicates(types); + // + // return maybeUnion(types); + // } else if (jsonschema.isAllOf(resolvedSchema)) { + // // FIXME: Do a proper thing here + // const firstResolved = resolvedSchema.allOf[0]; + // return schemaTypeToModelType(nameHint, resolve(firstResolved), fail); + // } else if (jsonschema.containsRelationship(resolvedSchema)) { + // // relationshipRef schema - treat as string as the type property is not present when they appear inside anyOf/oneOf + // return { type: 'string' }; + // } else { + // switch (resolvedSchema.type) { + // case 'string': + // if (resolvedSchema.format === 'timestamp') { + // return { type: 'date-time' }; + // } + // return { type: 'string' }; + // + // case 'array': + // // FIXME: insertionOrder, uniqueItems + // return using( + // schemaTypeToModelType(collectionNameHint(nameHint), resolve(resolvedSchema.items ?? true), fail), + // (element) => ({ + // type: 'array', + // element, + // }), + // ); + // + // case 'boolean': + // return { type: 'boolean' }; + // + // case 'object': + // return schemaObjectToModelType(nameHint, resolvedSchema, fail); + // + // case 'number': + // return { type: 'number' }; + // + // case 'integer': + // return { type: 'integer' }; + // + // case 'null': + // return { type: 'null' }; + // } + // } + // + // throw new Error('Unable to produce type'); + // }); + // } + // + // function validateCombiningSchemaType(schema: jsonschema.ConcreteSchema[], fail: Fail) { + // schema.forEach((element, index) => { + // if (!jsonschema.isAnyType(element) && !jsonschema.isCombining(element)) { + // schema.slice(index + 1).forEach((next) => { + // if (!jsonschema.isAnyType(next) && !jsonschema.isCombining(next)) { + // if (element.title === next.title && element.type !== next.type) { + // report.reportFailure( + // 'interpreting', + // fail(`Invalid schema with property name ${element.title} but types ${element.type} and ${next.type}`), + // ); + // } + // const elementName = jsonschema.resolvedReferenceName(element); + // const nextName = jsonschema.resolvedReferenceName(next); + // if (elementName && nextName && elementName === nextName && element.type !== next.type) { + // report.reportFailure( + // 'interpreting', + // fail(`Invalid schema with property name ${elementName} but types ${element.type} and ${next.type}`), + // ); + // } + // } + // }); + // } + // }); + // } + // + // function schemaObjectToModelType(nameHint: string, schema: jsonschema.Object, fail: Fail): Result { + // if (jsonschema.isMapLikeObject(schema)) { + // return mapLikeSchemaToModelType(nameHint, schema, fail); + // } else { + // return objectLikeSchemaToModelType(nameHint, schema, fail); + // } + // } + // + // function mapLikeSchemaToModelType( + // nameHint: string, + // schema: jsonschema.MapLikeObject, + // fail: Fail, + // ): Result { + // const innerNameHint = collectionNameHint(nameHint); + // + // // Map type. If 'patternProperties' is present we'll have it take precedence, because a lot of 'additionalProperties: true' are unintentially present. + // if (schema.patternProperties) { + // if (schema.additionalProperties === true) { + // report.reportFailure( + // 'interpreting', + // fail('additionalProperties: true is probably a mistake if patternProperties is also present'), + // ); + // } + // + // const unifiedPatternProps = fail.locate( + // locateFailure('patternProperties')( + // unionSchemas( + // ...Object.values(schema.patternProperties), + // // Use additionalProperties schema, but only if it's not 'true'. + // ...(schema.additionalProperties && schema.additionalProperties !== true + // ? [schema.additionalProperties] + // : []), + // ), + // ), + // ); + // + // return using(unifiedPatternProps, (unifiedType) => + // using(schemaTypeToModelType(innerNameHint, resolve(unifiedType), fail), (element) => ({ + // type: 'map', + // element, + // })), + // ); + // } else if (schema.additionalProperties) { + // return using(schemaTypeToModelType(innerNameHint, resolve(schema.additionalProperties), fail), (element) => ({ + // type: 'map', + // element, + // })); + // } + // + // // Fully untyped map that's not a type + // // @todo types should probably also just be json since they are useless otherwise. Fix after this package is in use. + // // FIXME: is 'json' really a primitive type, or do we mean `Map` or `Map` ? + // return { type: 'json' }; + // } + // + // function objectLikeSchemaToModelType( + // nameHint: string, + // schema: jsonschema.RecordLikeObject, + // fail: Fail, + // ): Result { + // if (looksLikeBuiltinTagType(schema)) { + // return { type: 'tag' }; + // } + // + // const { typeDefinitionBuilder, freshInSession } = eventBuilder.typeDefinitionBuilder(nameHint, { schema }); + // + // // If the type has no props, it's not a RecordLikeObject and we don't need to recurse + // // @todo The type should probably also just be json since they are useless otherwise. Fix after this package is in use. + // if (freshInSession) { + // if (schema.description) { + // typeDefinitionBuilder.setFields({ documentation: schema.description }); + // } + // if (jsonschema.isRecordLikeObject(schema)) { + // recurseProperties(schema, typeDefinitionBuilder, fail.in(`typedef ${nameHint}`)); + // } + // } + // + // return { type: 'ref', reference: ref(typeDefinitionBuilder.commit()) }; + // } + // + // function looksLikeBuiltinTagType(schema: jsonschema.Object): boolean { + // if (!jsonschema.isRecordLikeObject(schema)) { + // return false; + // } + // + // const eligibleTypeNames = ['Tag', 'Tags']; + // const expectedStringProperties = ['Key', 'Value']; + // + // const resolvedProps = expectedStringProperties.map((prop) => + // schema.properties[prop] ? resolve(schema.properties[prop]) : undefined, + // ); + // + // return ( + // Object.keys(schema.properties).length === resolvedProps.length && + // resolvedProps.every((x) => x !== undefined && jsonschema.isString(x)) && + // eligibleTypeNames.includes(lastWord(jsonschema.resolvedReferenceName(schema) ?? '')) + // ); + // } + // + // function describeDefault(schema: jsonschema.ConcreteSchema): string | undefined { + // if ( + // jsonschema.isAnyType(schema) || + // jsonschema.isAllOf(schema) || + // jsonschema.isAnyOf(schema) || + // jsonschema.isOneOf(schema) + // ) { + // return undefined; + // } + // + // switch (schema.type) { + // case 'string': + // case 'number': + // case 'integer': + // case 'boolean': + // return schema.default !== undefined ? JSON.stringify(schema.default) : undefined; + // } + // + // return undefined; + // } + // + // function handleTags(fail: Fail) { + // return tryCatch(fail, () => { + // const taggable = event?.tagging?.taggable ?? event.taggable ?? true; + // if (taggable) { + // const tagProp = simplePropNameFromJsonPtr(event.tagging?.tagProperty ?? '/properties/Tags'); + // const tagType = event.properties[tagProp]; + // if (!tagType) { + // report.reportFailure('interpreting', fail(`marked as taggable, but tagProperty does not exist: ${tagProp}`)); + // } else { + // const resolvedType = resolve(tagType); + // + // let variant: TagVariant = 'standard'; + // if (eventBuilder.cloudFormationType === 'AWS::AutoScaling::AutoScalingGroup') { + // variant = 'asg'; + // } else if (jsonschema.isObject(resolvedType) && jsonschema.isMapLikeObject(resolvedType)) { + // variant = 'map'; + // } + // + // eventBuilder.setTagInformation({ + // tagPropertyName: tagProp, + // variant, + // }); + // } + // } + // }); + // } + // + // /** + // * Derive a 'required' array from the oneOfs/anyOfs/allOfs in this source + // */ + // function calculateDefinitelyRequired(source: RequiredContainer): Set { + // const ret = new Set([...(source.required ?? [])]); + // + // if (source.oneOf) { + // setExtend(ret, setIntersect(...source.oneOf.map(calculateDefinitelyRequired))); + // } + // if (source.anyOf) { + // setExtend(ret, setIntersect(...source.anyOf.map(calculateDefinitelyRequired))); + // } + // if (source.allOf) { + // setExtend(ret, ...source.allOf.map(calculateDefinitelyRequired)); + // } + // + // return ret; + // } + // + // function withResult(x: Result, cb: (x: A) => void): void { + // if (isFailure(x)) { + // report.reportFailure('interpreting', x); + // } else { + // cb(x); + // } + // } + // + // function handleFailure(x: Result) { + // if (isFailure(x)) { + // report.reportFailure('interpreting', x); + // } + // } +} + +export interface LoadEventBridgeSchmemaOptions { + readonly db: SpecDatabase; + readonly event: EventBridgeSchema; + readonly report: ProblemReport; + readonly region?: string; +} diff --git a/packages/@aws-cdk/service-spec-importers/src/loaders/load-eventbridge-schema.ts b/packages/@aws-cdk/service-spec-importers/src/loaders/load-eventbridge-schema.ts new file mode 100644 index 000000000..339224d07 --- /dev/null +++ b/packages/@aws-cdk/service-spec-importers/src/loaders/load-eventbridge-schema.ts @@ -0,0 +1,71 @@ +import * as path from 'path'; +import * as util from 'util'; +import { isSuccess, Result } from '@cdklabs/tskb'; +import * as _glob from 'glob'; +import { Loader, LoadResult, LoadSourceOptions } from './loader'; +import { ProblemReport, ReportAudience } from '../report'; +import { EventBridgeSchema } from '../types'; + +const glob = util.promisify(_glob.glob); + +export interface EventBridgeSchemas { + readonly regionName: string; + readonly events: Array; +} + +export async function loadDefaultEventBridgeSchema( + schemaDir: string, + options: EventBridgeSchemaSourceOptions, +): Promise { + const files = await glob(`${schemaDir}/*`); + return Promise.all( + files.map(async (directoryName) => { + const regionName = path.basename(directoryName); + const events = await loadEventBridgeSchemaDirectory(schemaDir)(directoryName, options); + + return { regionName, events }; + }), + ); +} + +function loadEventBridgeSchemaDirectory( + baseDir: string, +): (directory: string, options: EventBridgeSchemaSourceOptions) => Promise { + console.log('SOMETHING WORKING....'); + return async (directory, options: EventBridgeSchemaSourceOptions) => { + const loader = await Loader.fromSchemaFile('EventBridge.schema.json', { + mustValidate: options.validate, + errorRootDirectory: baseDir, + }); + + const files = await glob(path.join(directory, '*.json')); + return loader.loadFiles(files, problemReportCombiner(options.report, options.failureAudience)); + }; +} + +export interface EventBridgeSchemas { + readonly regionName: string; + readonly events: Array; +} + +interface EventBridgeSchemaSourceOptions extends LoadSourceOptions { + readonly report: ProblemReport; + readonly failureAudience: ReportAudience; + // FIX: ReportAudience directing to cloudformation +} + +function problemReportCombiner(report: ProblemReport, failureAudience: ReportAudience) { + return (results: Result>[]): EventBridgeSchema[] => { + for (const r of results) { + if (isSuccess(r)) { + // const audience = ReportAudience.fromCloudFormationResource(r.value.typeName); + // report.reportFailure(audience, 'loading', ...r.warnings); + // report.reportPatch(audience, ...r.patchesApplied); + } else { + report.reportFailure(failureAudience, 'loading', r); + } + } + + return results.filter(isSuccess).map((r) => r.value); + }; +} diff --git a/packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts b/packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts new file mode 100644 index 000000000..fdbc7ee3c --- /dev/null +++ b/packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts @@ -0,0 +1,18 @@ +export interface EventBridgeSchema { + readonly SchemaName: string; + readonly Content: { + components: { + schemas: { + AWSEvent: { + 'x-amazon-events-detail-type': string; + 'x-amazon-events-source': string; + properties: { + detail: { + $ref: string; + }; + }; + }; + }; + }; + }; +} diff --git a/packages/@aws-cdk/service-spec-importers/src/types/index.ts b/packages/@aws-cdk/service-spec-importers/src/types/index.ts index 07fc81987..16a988753 100644 --- a/packages/@aws-cdk/service-spec-importers/src/types/index.ts +++ b/packages/@aws-cdk/service-spec-importers/src/types/index.ts @@ -7,3 +7,4 @@ export * from './stateful-resources/StatefulResources'; export * from './cloudwatch-console-service-directory/CloudWatchConsoleServiceDirectory'; export * from './getatt-allowlist/getatt-allowlist'; export * from './oob-relationships/OobRelationships'; +export * from './eventbridge/EventBridgeSchema'; diff --git a/packages/@aws-cdk/service-spec-types/src/index.ts b/packages/@aws-cdk/service-spec-types/src/index.ts index 567d20206..c5a71f513 100644 --- a/packages/@aws-cdk/service-spec-types/src/index.ts +++ b/packages/@aws-cdk/service-spec-types/src/index.ts @@ -1,5 +1,6 @@ export * from './types/database'; export * from './types/resource'; export * from './types/augmentations'; +export * from './types/event'; export * from './types/metrics'; export * from './types/diff'; diff --git a/packages/@aws-cdk/service-spec-types/src/types/database.ts b/packages/@aws-cdk/service-spec-types/src/types/database.ts index 830009ef2..003a475c7 100644 --- a/packages/@aws-cdk/service-spec-types/src/types/database.ts +++ b/packages/@aws-cdk/service-spec-types/src/types/database.ts @@ -2,6 +2,7 @@ import { promises as fs } from 'fs'; import { gunzipSync } from 'zlib'; import { Database, entityCollection, fieldIndex, stringCmp } from '@cdklabs/tskb'; import { IsAugmentedResource, ResourceAugmentation } from './augmentations'; +import { HasEvent, Event, EventUsesType } from './event'; import { DimensionSet, Metric, @@ -46,6 +47,9 @@ export function emptyDatabase() { dimensionSet: entityCollection().index({ dedupKey: fieldIndex('dedupKey', stringCmp), }), + event: entityCollection().index({ + name: fieldIndex('name', stringCmp), + }), }, (r) => ({ hasResource: r.relationship('service', 'resource'), @@ -58,6 +62,8 @@ export function emptyDatabase() { serviceHasMetric: r.relationship('service', 'metric'), resourceHasDimensionSet: r.relationship('resource', 'dimensionSet'), serviceHasDimensionSet: r.relationship('service', 'dimensionSet'), + hasEvent: r.relationship('resource', 'event'), + eventUsesType: r.relationship('event', 'typeDefinition'), }), ); } diff --git a/packages/@aws-cdk/service-spec-types/src/types/event.ts b/packages/@aws-cdk/service-spec-types/src/types/event.ts new file mode 100644 index 000000000..e042eb154 --- /dev/null +++ b/packages/@aws-cdk/service-spec-types/src/types/event.ts @@ -0,0 +1,21 @@ +import { Entity, Relationship } from '@cdklabs/tskb'; +import { PropertyType, Resource, TypeDefinition } from './resource'; + +export interface Event extends Entity { + readonly name: string; + readonly source: string; + readonly detailType: string; + readonly identifiersPath: Array; + // TODO: i think i need some type related to typeDefinition + readonly properties: EventProperties; +} + +export type HasEvent = Relationship; + +// FIX: looks like having 2 properties aren't a good idea :D +export type EventProperties = Record; +export interface EventProperty { + type: PropertyType; +} + +export type EventUsesType = Relationship; From b69878a058b8c69191eaac285dc42f379348de3b Mon Sep 17 00:00:00 2001 From: Gasser Date: Mon, 10 Nov 2025 16:32:02 +0100 Subject: [PATCH 02/12] all types working now --- .projenrc.ts | 1 + .../service-spec-importers/src/diff-fmt.ts | 1 + .../src/event-builder.ts | 685 +++++++++-------- .../importers/import-eventbridge-schema.ts | 716 ++++++++++-------- .../types/eventbridge/EventBridgeSchema.ts | 4 +- .../src/types/registry-schema/JsonSchema.ts | 10 +- .../service-spec-types/src/types/database.ts | 1 + .../service-spec-types/src/types/event.ts | 2 + 8 files changed, 785 insertions(+), 635 deletions(-) diff --git a/.projenrc.ts b/.projenrc.ts index d6c9d7e65..d25614764 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -137,6 +137,7 @@ const serviceSpecSchemaTask = serviceSpecImporters.addTask('gen-schemas', { 'CloudWatchConsoleServiceDirectory', 'GetAttAllowList', 'OobRelationshipData', + // TODO: should i add something here? ].map((typeName: string) => ({ exec: [ 'ts-json-schema-generator', diff --git a/packages/@aws-cdk/service-spec-importers/src/diff-fmt.ts b/packages/@aws-cdk/service-spec-importers/src/diff-fmt.ts index db6ba9c0c..d42cba371 100644 --- a/packages/@aws-cdk/service-spec-importers/src/diff-fmt.ts +++ b/packages/@aws-cdk/service-spec-importers/src/diff-fmt.ts @@ -33,6 +33,7 @@ export class DiffFormatter { constructor(db1: SpecDatabase, db2: SpecDatabase) { this.dbs = [db1, db2]; } + // TODO: I'm pretty sure i need to add something here to make my changes to show in the diff thingy public format(diff: SpecDatabaseDiff): string { const tree = new PrintableTree(); diff --git a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts index 236f8d193..79add65c8 100644 --- a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts @@ -1,5 +1,16 @@ -import { Service, SpecDatabase, Event, EventProperties } from '@aws-cdk/service-spec-types'; +import { + EventProperties, + EventProperty, + Property, + RichProperty, + Service, + SpecDatabase, + TypeDefinition, + Event, +} from '@aws-cdk/service-spec-types'; import { Entity, Reference } from '@cdklabs/tskb'; +import { AllFieldsGiven } from './diff-helpers'; +import { jsonschema } from './types'; // TODO: those aren't optional put them somehow required export interface EventBuilderOptions { @@ -7,16 +18,6 @@ export interface EventBuilderOptions { readonly detailType: string; } -// export interface Event extends Entity { -// readonly name: string; -// readonly source: string; -// readonly detailType: string; -// readonly identifiersPath: Array; -// readonly properties: Record>; -// } -// -// export type HasEvent = Relationship; - export class SpecBuilder { constructor(public readonly db: SpecDatabase) {} @@ -130,7 +131,7 @@ export class SpecBuilder { // TODO: change name to get? private allocateResource(service: Service) { - const resource = this.resourceDecider(service); + const resource = this.eventDecider(service); return resource; // TODO: I have no idea what i'm doing now :D, how the resource will not be in the DB? @@ -144,7 +145,8 @@ export class SpecBuilder { // return resource; } - private resourceDecider(service: Service) { + // TODO: change name to resource decider? + private eventDecider(service: Service) { // TODO: need to get all the requred property field names here const resources = this.db.follow('hasResource', service); if (service.name == 'aws-lambda') { @@ -161,14 +163,15 @@ interface ObjectWithProperties { } export class PropertyBagBuilder { - // protected candidateProperties: ResourceProperties = {}; - // + protected candidateProperties: EventProperties = {}; + // @ts-ignore constructor(private readonly _propertyBag: ObjectWithProperties) {} - // - // public setProperty(name: string, prop: Property) { - // this.candidateProperties[name] = prop; - // } + + public setProperty(name: string, prop: EventProperty) { + console.log('Setting property', { prop, name }); + this.candidateProperties[name] = prop; + } // // /** // * Delete a property from the builder @@ -179,331 +182,363 @@ export class PropertyBagBuilder { // public unsetProperty(name: string) { // delete this.candidateProperties[name]; // } + + /** + * Commit the property and attribute changes to the underlying property bag. + */ + public commit(): ObjectWithProperties { + for (const [name, prop] of Object.entries(this.candidateProperties)) { + this.commitProperty(name, prop); + } + + return this._propertyBag; + } + + private commitProperty(name: string, prop: Property) { + if (this._propertyBag.properties[name]) { + this.mergeProperty(this._propertyBag.properties[name], prop); + } else { + this._propertyBag.properties[name] = prop; + } + this.simplifyProperty(this._propertyBag.properties[name]); + } + + protected mergeProperty(prop: Property, updates: Property) { + // This handles merges that are trivial scalar overwrites. All + // fields must be represented, if you have special code to handle + // a field, put it in here as 'undefined' and add code to handle it below. + copyDefined({ + causesReplacement: updates.causesReplacement, + defaultValue: updates.defaultValue, + deprecated: updates.deprecated, + documentation: updates.documentation, + required: updates.required, + scrutinizable: updates.scrutinizable, + relationshipRefs: updates.relationshipRefs, + + // These will be handled specially below + previousTypes: undefined, + type: undefined, + }); + + // Special field handling + for (const type of updates.previousTypes ?? []) { + new RichProperty(prop).updateType(type); + } + new RichProperty(prop).updateType(updates.type); + + function copyDefined(upds: AllFieldsGiven>) { + for (const [key, value] of Object.entries(upds)) { + if (value !== undefined) { + (prop as any)[key] = value; + } + } + } + } + + /** + * Remove settings that are equal to their defaults + */ + protected simplifyProperty(prop: Property) { + if (!prop.required) { + delete prop.required; + } + if (prop.causesReplacement === 'no') { + delete prop.causesReplacement; + } + } +} + +export class EventBuilder extends PropertyBagBuilder { + private typeDefinitions = new Map(); + private typesCreatedHere = new Set(); // - // /** - // * Commit the property and attribute changes to the underlying property bag. - // */ - // public commit(): ObjectWithProperties { - // for (const [name, prop] of Object.entries(this.candidateProperties)) { - // this.commitProperty(name, prop); + // /** + // * Keep a copy of all properties configured here + // * + // * We'll need some of them later to turn them into attributes. + // */ + // private allProperties: ResourceProperties = {}; + // + // private candidateAttributes: ResourceProperties = {}; + // + + // @ts-ignore + constructor(public readonly db: SpecDatabase, private readonly event: Event) { + super(event); + // this.indexExistingTypeDefinitions(); + } + // + // public get cloudFormationType(): string { + // return this.event.cloudFormationType; + // } + // + // public setProperty(name: string, prop: Property) { + // super.setProperty(name, prop); + // this.allProperties[name] = prop; + // } + // + // public setAttribute(name: string, attr: Attribute) { + // this.candidateAttributes[name] = attr; + // } + // + // public unsetAttribute(name: string) { + // delete this.candidateAttributes[name]; + // } + // + // /** + // * Mark the given properties as attributes instead + // * + // * These can be simple property names (`Foo`, `Bar`), but they can also be + // * compound property names (`Foo/Bar`), and the compound property names can + // * contain array wildcards (`Foo/*­/Bar`). + // * + // * In the CloudFormation resource spec, compound property names are separated + // * by periods (`Foo.Bar`). + // * + // * In upconverted CloudFormation resource specs -> registry specs, the compound + // * property name references may contain a period, while the actual property name + // * in the properties bag has the periods stripped: attributeName is `Foo.Bar`, + // * but the actual property name is `FooBar`. + // * + // * The same deep property name may occur multiple times (`Foo`, `Foo/Bar`, `Foo/Baz`). + // */ + // public markAsAttributes(props: string[]) { + // for (const propName of props) { + // if (this.candidateProperties[propName]) { + // this.setAttribute(propName, this.candidateProperties[propName]); + // this.unsetProperty(propName); + // continue; // } // - // return this._propertyBag; + // // In case of a half-upconverted legacy spec, the property might also + // // exist with a name that has any `.` stripped. + // const strippedName = stripPeriods(propName); + // if (this.candidateProperties[strippedName]) { + // // The ACTUAL name is still the name with '.' in it, but we copy the type + // // from the stripped name. + // this.setAttribute(propName, this.candidateProperties[strippedName]); + // this.unsetProperty(strippedName); + // continue; + // } + // + // // Otherwise assume the name represents a compound attribute + // // In the Registry spec, compound attributes will look like 'Container/Prop'. + // // In the legacy spec they will look like 'Container.Prop'. + // // Some Registry resources incorrectly use '.' as well. + // // We accept both here, but turn them both into '.'-separated. + // // + // // Sometimes this contains a `*`, to indicate that it could be any element in an array. + // // We can't currently support those, so we drop them (ex: `Subscribers/*/Status`). + // // + // // We don't remove the top-level properties from the resource, we just add the attributes. + // const propPath = propName.split(/[\.\/]/); + // const propWithPeriods = propPath.join('.'); + // if (propPath.includes('*')) { + // // Skip unrepresentable + // continue; + // } + // + // try { + // const prop = this.propertyDeep(...propPath); + // if (prop) { + // this.setAttribute(propWithPeriods, prop); + // + // // FIXME: not sure if we need to delete property `Foo` if the only + // // attribute reference we got is `Foo/Bar`. Let's not for now. + // } + // } catch (e: any) { + // // We're catching any errors from propertyDeep because CloudFormation allows schemas + // // where attribute properties are not part of the spec anywhere else. Although it is + // // likely a bad schema, CDK forges ahead by just dropping the attribute. + // // Example: `ProviderDetails` typed as `Map` and `"readOnlyProperties: ['/properties/ProviderDetails/Attribute']"` + // console.log(`Attribute cannot be found in the spec. Error returned: ${e}.`); + // } // } + // } + // + // /** + // * Mark the given properties as immutable + // * + // * This be a top-level property reference, or a deep property reference, like `Foo` or + // * `Foo/Bar`. + // */ + // public markAsImmutable(props: string[]) { + // for (const propName of props) { + // const propPath = propName.split(/\//); // - // private commitProperty(name: string, prop: Property) { - // if (this._propertyBag.properties[name]) { - // this.mergeProperty(this._propertyBag.properties[name], prop); - // } else { - // this._propertyBag.properties[name] = prop; + // try { + // const prop = this.propertyDeep(...propPath); + // if (prop) { + // prop.causesReplacement = 'yes'; + // } + // } catch { + // if (!this.event.additionalReplacementProperties) { + // this.event.additionalReplacementProperties = []; + // } + // this.event.additionalReplacementProperties.push(propPath); // } - // this.simplifyProperty(this._propertyBag.properties[name]); // } + // } + // + // public markDeprecatedProperties(...props: string[]) { + // for (const propName of props) { + // (this.candidateProperties[propName] ?? {}).deprecated = Deprecation.WARN; + // } + // } // - // protected mergeProperty(prop: Property, updates: Property) { - // // This handles merges that are trivial scalar overwrites. All - // // fields must be represented, if you have special code to handle - // // a field, put it in here as 'undefined' and add code to handle it below. - // copyDefined({ - // causesReplacement: updates.causesReplacement, - // defaultValue: updates.defaultValue, - // deprecated: updates.deprecated, - // documentation: updates.documentation, - // required: updates.required, - // scrutinizable: updates.scrutinizable, - // relationshipRefs: updates.relationshipRefs, + // public setTagInformation(tagInfo: TagInformation) { + // this.event.tagInformation = tagInfo; + // } // - // // These will be handled specially below - // previousTypes: undefined, - // type: undefined, - // }); + // public propertyDeep(...fieldPath: string[]): Property | undefined { + // // The property bag we're searching in. Start by searching 'allProperties', not + // // the current set of resource props (as markAsAttributes may have deleted some of them) + // let currentBag: ResourceProperties = this.allProperties; // - // // Special field handling - // for (const type of updates.previousTypes ?? []) { - // new RichProperty(prop).updateType(type); + // for (let i = 0; i < fieldPath.length - 1; i++) { + // const prop = currentBag[fieldPath[i]]; + // if (!prop) { + // throw new Error( + // `${this.event.cloudFormationType}: no definition for property: ${fieldPath.slice(0, i + 1).join('/')}`, + // ); // } - // new RichProperty(prop).updateType(updates.type); // - // function copyDefined(upds: AllFieldsGiven>) { - // for (const [key, value] of Object.entries(upds)) { - // if (value !== undefined) { - // (prop as any)[key] = value; - // } + // let propType = prop.type; + // + // // Handle '*'s + // while (fieldPath[i + 1] === '*') { + // if (propType.type !== 'array' && propType.type !== 'map') { + // throw new Error( + // `${this.event.cloudFormationType}: ${fieldPath.join('/')}: expected array but ${fieldPath + // .slice(0, i + 1) + // .join('/')} is a ${new RichPropertyType(propType).stringify(this.db)}`, + // ); // } - // } - // } // - // /** - // * Remove settings that are equal to their defaults - // */ - // protected simplifyProperty(prop: Property) { - // if (!prop.required) { - // delete prop.required; + // propType = propType.element; + // i += 1; // } - // if (prop.causesReplacement === 'no') { - // delete prop.causesReplacement; + // + // if (propType.type !== 'ref') { + // throw new Error( + // `${this.event.cloudFormationType}: ${fieldPath.join('/')}: expected type definition but ${fieldPath + // .slice(0, i + 1) + // .join('/')} is a ${new RichPropertyType(propType).stringify(this.db)}`, + // ); // } + // + // const typeDef = this.db.get('typeDefinition', propType.reference); + // currentBag = typeDef.properties; + // } + // + // return currentBag[fieldPath[fieldPath.length - 1]]; + // } + // + public typeDefinitionBuilder( + typeName: string, + options?: { description?: string; schema?: jsonschema.RecordLikeObject }, + ) { + const existing = this.typeDefinitions.get(typeName); + const description = options?.description; + const freshInSession = !this.typesCreatedHere.has(typeName); + this.typesCreatedHere.add(typeName); + + if (existing) { + if (!existing.documentation && description) { + existing.documentation = description; + } + const properties = options?.schema?.properties ?? {}; + // If db already contains typeName's type definition, we want to additionally + // check if the schema matches the type definition. If the schema includes new + // properties, we want to add them to the type definition. + if (!Object.keys(properties).every((element) => Object.keys(existing.properties).includes(element))) { + return { + typeDefinitionBuilder: new TypeDefinitionBuilder(this.db, existing), + freshInDb: true, + freshInSession: true, + }; + } + return { + typeDefinitionBuilder: new TypeDefinitionBuilder(this.db, existing), + freshInDb: false, + freshInSession, + }; + } + + const typeDef = this.db.allocate('typeDefinition', { + name: typeName, + documentation: description, + properties: {}, + }); + this.db.link('eventUsesType', this.event, typeDef); + this.typeDefinitions.set(typeName, typeDef); + + const builder = new TypeDefinitionBuilder(this.db, typeDef); + return { typeDefinitionBuilder: builder, freshInDb: true, freshInSession }; + } + + /** + * Commit the property and attribute changes to the resource. + */ + public commit(): Event { + // Commit properties + super.commit(); + + // for (const [name, attr] of Object.entries(this.candidateAttributes)) { + // this.commitAttribute(name, attr); + // } + + return this.event; + } + + // private commitAttribute(name: string, attr: Attribute) { + // if (this.event.attributes[name]) { + // this.mergeProperty(this.event.attributes[name], attr); + // } else { + // this.event.attributes[name] = attr; + // } + // this.simplifyProperty(this.event.attributes[name]); + // } + + // /** + // * Index the existing type definitions currently in the DB + // */ + // private indexExistingTypeDefinitions() { + // for (const { entity: typeDef } of this.db.follow('usesType', this.event)) { + // this.typeDefinitions.set(typeDef.name, typeDef); // } + // } } -export class EventBuilder extends PropertyBagBuilder { - // // private typeDefinitions = new Map(); - // // private typesCreatedHere = new Set(); - // // - // // /** - // // * Keep a copy of all properties configured here - // // * - // // * We'll need some of them later to turn them into attributes. - // // */ - // // private allProperties: ResourceProperties = {}; - // // - // // private candidateAttributes: ResourceProperties = {}; - // // +export type TypeDefinitionFields = Pick; - // @ts-ignore - constructor(public readonly db: SpecDatabase, private readonly event: Event) { - super(event); - // this.indexExistingTypeDefinitions(); +export class TypeDefinitionBuilder extends PropertyBagBuilder { + private readonly fields: TypeDefinitionFields = {}; + + constructor(public readonly db: SpecDatabase, private readonly typeDef: TypeDefinition) { + super(typeDef); + } + + public setFields(fields: TypeDefinitionFields) { + Object.assign(this.fields, fields); + } + + public commit(): TypeDefinition { + super.commit(); + Object.assign(this.typeDef, this.fields); + return this.typeDef; } - // // - // // public get cloudFormationType(): string { - // // return this.resource.cloudFormationType; - // // } - // // - // // public setProperty(name: string, prop: Property) { - // // super.setProperty(name, prop); - // // this.allProperties[name] = prop; - // // } - // // - // // public setAttribute(name: string, attr: Attribute) { - // // this.candidateAttributes[name] = attr; - // // } - // // - // // public unsetAttribute(name: string) { - // // delete this.candidateAttributes[name]; - // // } - // // - // // /** - // // * Mark the given properties as attributes instead - // // * - // // * These can be simple property names (`Foo`, `Bar`), but they can also be - // // * compound property names (`Foo/Bar`), and the compound property names can - // // * contain array wildcards (`Foo/*­/Bar`). - // // * - // // * In the CloudFormation resource spec, compound property names are separated - // // * by periods (`Foo.Bar`). - // // * - // // * In upconverted CloudFormation resource specs -> registry specs, the compound - // // * property name references may contain a period, while the actual property name - // // * in the properties bag has the periods stripped: attributeName is `Foo.Bar`, - // // * but the actual property name is `FooBar`. - // // * - // // * The same deep property name may occur multiple times (`Foo`, `Foo/Bar`, `Foo/Baz`). - // // */ - // // public markAsAttributes(props: string[]) { - // // for (const propName of props) { - // // if (this.candidateProperties[propName]) { - // // this.setAttribute(propName, this.candidateProperties[propName]); - // // this.unsetProperty(propName); - // // continue; - // // } - // // - // // // In case of a half-upconverted legacy spec, the property might also - // // // exist with a name that has any `.` stripped. - // // const strippedName = stripPeriods(propName); - // // if (this.candidateProperties[strippedName]) { - // // // The ACTUAL name is still the name with '.' in it, but we copy the type - // // // from the stripped name. - // // this.setAttribute(propName, this.candidateProperties[strippedName]); - // // this.unsetProperty(strippedName); - // // continue; - // // } - // // - // // // Otherwise assume the name represents a compound attribute - // // // In the Registry spec, compound attributes will look like 'Container/Prop'. - // // // In the legacy spec they will look like 'Container.Prop'. - // // // Some Registry resources incorrectly use '.' as well. - // // // We accept both here, but turn them both into '.'-separated. - // // // - // // // Sometimes this contains a `*`, to indicate that it could be any element in an array. - // // // We can't currently support those, so we drop them (ex: `Subscribers/*/Status`). - // // // - // // // We don't remove the top-level properties from the resource, we just add the attributes. - // // const propPath = propName.split(/[\.\/]/); - // // const propWithPeriods = propPath.join('.'); - // // if (propPath.includes('*')) { - // // // Skip unrepresentable - // // continue; - // // } - // // - // // try { - // // const prop = this.propertyDeep(...propPath); - // // if (prop) { - // // this.setAttribute(propWithPeriods, prop); - // // - // // // FIXME: not sure if we need to delete property `Foo` if the only - // // // attribute reference we got is `Foo/Bar`. Let's not for now. - // // } - // // } catch (e: any) { - // // // We're catching any errors from propertyDeep because CloudFormation allows schemas - // // // where attribute properties are not part of the spec anywhere else. Although it is - // // // likely a bad schema, CDK forges ahead by just dropping the attribute. - // // // Example: `ProviderDetails` typed as `Map` and `"readOnlyProperties: ['/properties/ProviderDetails/Attribute']"` - // // console.log(`Attribute cannot be found in the spec. Error returned: ${e}.`); - // // } - // // } - // // } - // // - // // /** - // // * Mark the given properties as immutable - // // * - // // * This be a top-level property reference, or a deep property reference, like `Foo` or - // // * `Foo/Bar`. - // // */ - // // public markAsImmutable(props: string[]) { - // // for (const propName of props) { - // // const propPath = propName.split(/\//); - // // - // // try { - // // const prop = this.propertyDeep(...propPath); - // // if (prop) { - // // prop.causesReplacement = 'yes'; - // // } - // // } catch { - // // if (!this.resource.additionalReplacementProperties) { - // // this.resource.additionalReplacementProperties = []; - // // } - // // this.resource.additionalReplacementProperties.push(propPath); - // // } - // // } - // // } - // // - // // public markDeprecatedProperties(...props: string[]) { - // // for (const propName of props) { - // // (this.candidateProperties[propName] ?? {}).deprecated = Deprecation.WARN; - // // } - // // } - // // - // // public setTagInformation(tagInfo: TagInformation) { - // // this.resource.tagInformation = tagInfo; - // // } - // // - // // public propertyDeep(...fieldPath: string[]): Property | undefined { - // // // The property bag we're searching in. Start by searching 'allProperties', not - // // // the current set of resource props (as markAsAttributes may have deleted some of them) - // // let currentBag: ResourceProperties = this.allProperties; - // // - // // for (let i = 0; i < fieldPath.length - 1; i++) { - // // const prop = currentBag[fieldPath[i]]; - // // if (!prop) { - // // throw new Error( - // // `${this.resource.cloudFormationType}: no definition for property: ${fieldPath.slice(0, i + 1).join('/')}`, - // // ); - // // } - // // - // // let propType = prop.type; - // // - // // // Handle '*'s - // // while (fieldPath[i + 1] === '*') { - // // if (propType.type !== 'array' && propType.type !== 'map') { - // // throw new Error( - // // `${this.resource.cloudFormationType}: ${fieldPath.join('/')}: expected array but ${fieldPath - // // .slice(0, i + 1) - // // .join('/')} is a ${new RichPropertyType(propType).stringify(this.db)}`, - // // ); - // // } - // // - // // propType = propType.element; - // // i += 1; - // // } - // // - // // if (propType.type !== 'ref') { - // // throw new Error( - // // `${this.resource.cloudFormationType}: ${fieldPath.join('/')}: expected type definition but ${fieldPath - // // .slice(0, i + 1) - // // .join('/')} is a ${new RichPropertyType(propType).stringify(this.db)}`, - // // ); - // // } - // // - // // const typeDef = this.db.get('typeDefinition', propType.reference); - // // currentBag = typeDef.properties; - // // } - // // - // // return currentBag[fieldPath[fieldPath.length - 1]]; - // // } - // // - // // public typeDefinitionBuilder( - // // typeName: string, - // // options?: { description?: string; schema?: jsonschema.RecordLikeObject }, - // // ) { - // // const existing = this.typeDefinitions.get(typeName); - // // const description = options?.description; - // // const freshInSession = !this.typesCreatedHere.has(typeName); - // // this.typesCreatedHere.add(typeName); - // // - // // if (existing) { - // // if (!existing.documentation && description) { - // // existing.documentation = description; - // // } - // // const properties = options?.schema?.properties ?? {}; - // // // If db already contains typeName's type definition, we want to additionally - // // // check if the schema matches the type definition. If the schema includes new - // // // properties, we want to add them to the type definition. - // // if (!Object.keys(properties).every((element) => Object.keys(existing.properties).includes(element))) { - // // return { - // // typeDefinitionBuilder: new TypeDefinitionBuilder(this.db, existing), - // // freshInDb: true, - // // freshInSession: true, - // // }; - // // } - // // return { - // // typeDefinitionBuilder: new TypeDefinitionBuilder(this.db, existing), - // // freshInDb: false, - // // freshInSession, - // // }; - // // } - // // - // // const typeDef = this.db.allocate('typeDefinition', { - // // name: typeName, - // // documentation: description, - // // properties: {}, - // // }); - // // this.db.link('usesType', this.resource, typeDef); - // // this.typeDefinitions.set(typeName, typeDef); - // // - // // const builder = new TypeDefinitionBuilder(this.db, typeDef); - // // return { typeDefinitionBuilder: builder, freshInDb: true, freshInSession }; - // // } - // // - // // /** - // // * Commit the property and attribute changes to the resource. - // // */ - // // public commit(): Resource { - // // // Commit properties - // // super.commit(); - // // - // // for (const [name, attr] of Object.entries(this.candidateAttributes)) { - // // this.commitAttribute(name, attr); - // // } - // // - // // return this.resource; - // // } - // // - // // private commitAttribute(name: string, attr: Attribute) { - // // if (this.resource.attributes[name]) { - // // this.mergeProperty(this.resource.attributes[name], attr); - // // } else { - // // this.resource.attributes[name] = attr; - // // } - // // this.simplifyProperty(this.resource.attributes[name]); - // // } - // // - // // /** - // // * Index the existing type definitions currently in the DB - // // */ - // // private indexExistingTypeDefinitions() { - // // for (const { entity: typeDef } of this.db.follow('usesType', this.resource)) { - // // this.typeDefinitions.set(typeDef.name, typeDef); - // // } - // // } } + +// function last(xs: A[]): A { +// return xs[xs.length - 1]; +// } + +/** + * Turns a compound name into its property equivalent + * Compliance.Type -> ComplianceType + */ +// function stripPeriods(name: string) { +// return name.split('.').join(''); +// } diff --git a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts index fdeffc11b..4114e0c61 100644 --- a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts @@ -1,8 +1,10 @@ -import { SpecDatabase } from '@aws-cdk/service-spec-types'; -import { failure } from '@cdklabs/tskb'; -import { SpecBuilder } from '../event-builder'; +import { PropertyType, RichPropertyType, SpecDatabase } from '@aws-cdk/service-spec-types'; +import { locateFailure, Fail, failure, isFailure, Result, tryCatch, using, ref, isSuccess } from '@cdklabs/tskb'; +import { SpecBuilder, PropertyBagBuilder } from '../event-builder'; import { ProblemReport, ReportAudience } from '../report'; -import { EventBridgeSchema } from '../types'; +import { unionSchemas } from '../schema-manipulation/unify-schemas'; +import { maybeUnion } from '../type-manipulation'; +import { EventBridgeSchema, ImplicitJsonSchemaRecord, jsonschema } from '../types'; export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) { const { db, event } = options; @@ -16,10 +18,35 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) source: event.Content.components.schemas.AWSEvent['x-amazon-events-source'], detailType: event.Content.components.schemas.AWSEvent['x-amazon-events-detail-type'], }); + + if (eventBuilder == undefined) { + return; + } // @ts-ignore const eventFailure = failure.in(event.SchemaName); - // recurseProperties(event, eventBuilder, eventFailure); + console.log('here', { properties: event.properties }); + // TODO: copied from [this part](https://github.com/cdklabs/awscdk-service-spec/blob/main/packages/%40aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts#L397-L406) + // Does it make sense to put it in a function + + // FIX: jsonschema pointing toward cloudformation thing + const resolve = jsonschema.makeResolver(event); + + // const parts = event.Content.components.schemas.AWSEvent.properties.detail.$ref.substring(2).split('/'); + // let current = event.Content; + // let lastKey: string | undefined; + // while (true) { + // if (parts.length === 0) { + // break; + // } + // lastKey = parts.shift()!; + // // @ts-ignore + // current = current[lastKey]; + // } + + // FIX: there's some problem here event.properties exist for some reason + // @ts-ignore + recurseProperties(event.Content.components.schemas.AWSEvent, eventBuilder, eventFailure); // // // AWS::CloudFront::ContinuousDeploymentPolicy recently introduced a change where they're marking deprecatedProperties // // as `/definitions//properties/` instead of `/properties///`. Ignore those, as it's @@ -38,50 +65,57 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) // eventBuilder.markAsImmutable((event.createOnlyProperties ?? []).map(simplePropNameFromJsonPtr)); // // handleFailure(handleTags(eventFailure)); - // return eventBuilder.commit(); - // - // // FIX: jsonschema pointing toward cloudformation thing - // const resolve = jsonschema.makeResolver(event); - // function recurseProperties(source: ImplicitJsonSchemaRecord, target: PropertyBagBuilder, fail: Fail) { - // if (!source.properties) { - // throw new Error(`Not an object type with properties: ${JSON.stringify(source)}`); - // } - // - // const required = calculateDefinitelyRequired(source); - // - // for (const [name, property] of Object.entries(source.properties)) { - // try { - // let resolvedSchema = resolve(property); - // const relationships = collectPossibleRelationships(resolvedSchema); - // withResult(schemaTypeToModelType(name, resolvedSchema, fail.in(`property ${name}`)), (type) => { - // target.setProperty(name, { - // type, - // documentation: descriptionOf(resolvedSchema), - // required: required.has(name), - // defaultValue: describeDefault(resolvedSchema), - // relationshipRefs: relationships.length > 0 ? relationships : undefined, - // }); - // }); - // } catch (e) { - // report.reportFailure( - // 'interpreting', - // fail(`Skip generating property ${name} for resource ${event.typeName} because of ${e}`), - // ); - // } - // } + + // if (event.SchemaName == 'aws.a4b@RoomStateChange') { + // console.log({}); // } - // - // /** - // * Recursively extracts relationshipRef metadata from JSON schemas - // * - // * Finds relationshipRef objects that indicate this property references - // * another CloudFormation resource, and converts property paths to name. - // * This function handles the following cases: - // * Single relationship for property -> { type: 'string', relationshipRef: {...} } - // * When there are multiple relationships or the relationships are nested behind - // * layers of oneOf/anyOf/allOf the data looks like - // * { type: 'string', anyOf: [ { relationshipRef: {...} }, ...]} - // */ + return eventBuilder.commit(); + + // FIX: i need to pass the specific detail object not like CF schema + function recurseProperties(source: ImplicitJsonSchemaRecord, target: PropertyBagBuilder, fail: Fail) { + if (!source.properties) { + console.log('BUG', { event: JSON.stringify(event) }); + throw new Error(`Not an object type with properties: ${JSON.stringify(source)}`); + } + + const required = calculateDefinitelyRequired(source); + + for (const [name, property] of Object.entries(source.properties)) { + try { + console.log('looping over the properties', { name, property }); + // FIX: this boolean should be something else + let resolvedSchema = resolve(property, true); + console.log({ resolvedSchema }); + // const relationships = collectPossibleRelationships(resolvedSchema); + withResult(schemaTypeToModelType(name, resolvedSchema, fail.in(`property ${name}`)), (type) => { + target.setProperty(name, { + type, + // documentation: descriptionOf(resolvedSchema), + required: required.has(name), + // defaultValue: describeDefault(resolvedSchema), + // relationshipRefs: relationships.length > 0 ? relationships : undefined, + }); + }); + } catch (e) { + report.reportFailure( + 'interpreting', + fail(`Skip generating property ${name} for resource ${event.SchemaName} because of ${e}`), + ); + } + } + } + + /** + * Recursively extracts relationshipRef metadata from JSON schemas + * + * Finds relationshipRef objects that indicate this property references + * another CloudFormation resource, and converts property paths to name. + * This function handles the following cases: + * Single relationship for property -> { type: 'string', relationshipRef: {...} } + * When there are multiple relationships or the relationships are nested behind + * layers of oneOf/anyOf/allOf the data looks like + * { type: 'string', anyOf: [ { relationshipRef: {...} }, ...]} + */ // function collectPossibleRelationships(schema: jsonschema.ConcreteSchema): RelationshipRef[] { // if (jsonschema.isAnyType(schema)) { // return []; @@ -101,241 +135,245 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) // } // return []; // } - // - // /** - // * Convert a JSON schema type to a type in the database model - // */ - // function schemaTypeToModelType( - // propertyName: string, - // resolvedSchema: jsonschema.ResolvedSchema, - // fail: Fail, - // ): Result { - // return tryCatch(fail, (): Result => { - // const reference = jsonschema.resolvedReference(resolvedSchema); - // const referenceName = jsonschema.resolvedReferenceName(resolvedSchema); - // const nameHint = referenceName ? lastWord(referenceName) : lastWord(propertyName); - // - // if (jsonschema.isAnyType(resolvedSchema)) { - // return { type: 'json' }; - // } else if (jsonschema.isOneOf(resolvedSchema) || jsonschema.isAnyOf(resolvedSchema)) { - // const inner = jsonschema.innerSchemas(resolvedSchema); - // // The union type is a type definition - // // This is something we don't support at the moment as it's effectively a XOR for the property type - // // In future we should create an enum like class for this, but we cannot at the moment to maintain backwards compatibility - // // For now we assume the union is merged into a single object - // if (reference && inner.every((s) => jsonschema.isObject(s))) { - // report.reportFailure( - // 'interpreting', - // fail(`Ref ${referenceName} is a union of objects. Merging into a single type.`), - // ); - // const combinedType = unionSchemas(...inner) as jsonschema.ConcreteSchema; - // if (isFailure(combinedType)) { - // return combinedType; - // } - // return schemaTypeToModelType(nameHint, jsonschema.setResolvedReference(combinedType, reference), fail); - // } - // - // // Validate oneOf and anyOf types schema by validating whether there are two definitions in oneOf/anyOf - // // that has the same property name but different types. For simplicity, we do not validate if the types - // // are overlapping. We will add this case to the problem report. An sample schema would be i.e. - // // foo: { oneOf: [ { properties: { type: ObjectA } }, { properties: { type: ObjectB } }]} - // validateCombiningSchemaType(inner, fail); - // - // const convertedTypes = inner.map((t) => { - // if (jsonschema.isObject(t) && jsonschema.isRecordLikeObject(t)) { - // // The item in union type is an object with properties - // // We need to remove 'required' constraint from the object schema definition as we're dealing - // // with oneOf/anyOf. Note that we should ONLY remove 'required' when the 'required' constraint - // // refers to the object itself not the inner properties - // const refName = jsonschema.resolvedReferenceName(t); - // if ((t.title && t.required?.includes(t.title)) || (refName && t.required?.includes(refName))) { - // report.reportFailure( - // 'interpreting', - // fail( - // `${propertyName} is a union of objects. Merging into a single type and removing required fields for oneOf and anyOf.`, - // ), - // ); - // return schemaTypeToModelType(nameHint, resolve({ ...t, required: undefined }), fail); - // } - // } - // return schemaTypeToModelType(nameHint, resolve(t), fail); - // }); - // report.reportFailure('interpreting', ...convertedTypes.filter(isFailure)); - // - // const types = convertedTypes.filter(isSuccess); - // removeUnionDuplicates(types); - // - // return maybeUnion(types); - // } else if (jsonschema.isAllOf(resolvedSchema)) { - // // FIXME: Do a proper thing here - // const firstResolved = resolvedSchema.allOf[0]; - // return schemaTypeToModelType(nameHint, resolve(firstResolved), fail); - // } else if (jsonschema.containsRelationship(resolvedSchema)) { - // // relationshipRef schema - treat as string as the type property is not present when they appear inside anyOf/oneOf - // return { type: 'string' }; - // } else { - // switch (resolvedSchema.type) { - // case 'string': - // if (resolvedSchema.format === 'timestamp') { - // return { type: 'date-time' }; - // } - // return { type: 'string' }; - // - // case 'array': - // // FIXME: insertionOrder, uniqueItems - // return using( - // schemaTypeToModelType(collectionNameHint(nameHint), resolve(resolvedSchema.items ?? true), fail), - // (element) => ({ - // type: 'array', - // element, - // }), - // ); - // - // case 'boolean': - // return { type: 'boolean' }; - // - // case 'object': - // return schemaObjectToModelType(nameHint, resolvedSchema, fail); - // - // case 'number': - // return { type: 'number' }; - // - // case 'integer': - // return { type: 'integer' }; - // - // case 'null': - // return { type: 'null' }; - // } - // } - // - // throw new Error('Unable to produce type'); - // }); - // } - // - // function validateCombiningSchemaType(schema: jsonschema.ConcreteSchema[], fail: Fail) { - // schema.forEach((element, index) => { - // if (!jsonschema.isAnyType(element) && !jsonschema.isCombining(element)) { - // schema.slice(index + 1).forEach((next) => { - // if (!jsonschema.isAnyType(next) && !jsonschema.isCombining(next)) { - // if (element.title === next.title && element.type !== next.type) { - // report.reportFailure( - // 'interpreting', - // fail(`Invalid schema with property name ${element.title} but types ${element.type} and ${next.type}`), - // ); - // } - // const elementName = jsonschema.resolvedReferenceName(element); - // const nextName = jsonschema.resolvedReferenceName(next); - // if (elementName && nextName && elementName === nextName && element.type !== next.type) { - // report.reportFailure( - // 'interpreting', - // fail(`Invalid schema with property name ${elementName} but types ${element.type} and ${next.type}`), - // ); - // } - // } - // }); - // } - // }); - // } - // - // function schemaObjectToModelType(nameHint: string, schema: jsonschema.Object, fail: Fail): Result { - // if (jsonschema.isMapLikeObject(schema)) { - // return mapLikeSchemaToModelType(nameHint, schema, fail); - // } else { - // return objectLikeSchemaToModelType(nameHint, schema, fail); - // } - // } - // - // function mapLikeSchemaToModelType( - // nameHint: string, - // schema: jsonschema.MapLikeObject, - // fail: Fail, - // ): Result { - // const innerNameHint = collectionNameHint(nameHint); - // - // // Map type. If 'patternProperties' is present we'll have it take precedence, because a lot of 'additionalProperties: true' are unintentially present. - // if (schema.patternProperties) { - // if (schema.additionalProperties === true) { - // report.reportFailure( - // 'interpreting', - // fail('additionalProperties: true is probably a mistake if patternProperties is also present'), - // ); - // } - // - // const unifiedPatternProps = fail.locate( - // locateFailure('patternProperties')( - // unionSchemas( - // ...Object.values(schema.patternProperties), - // // Use additionalProperties schema, but only if it's not 'true'. - // ...(schema.additionalProperties && schema.additionalProperties !== true - // ? [schema.additionalProperties] - // : []), - // ), - // ), - // ); - // - // return using(unifiedPatternProps, (unifiedType) => - // using(schemaTypeToModelType(innerNameHint, resolve(unifiedType), fail), (element) => ({ - // type: 'map', - // element, - // })), - // ); - // } else if (schema.additionalProperties) { - // return using(schemaTypeToModelType(innerNameHint, resolve(schema.additionalProperties), fail), (element) => ({ - // type: 'map', - // element, - // })); - // } - // - // // Fully untyped map that's not a type - // // @todo types should probably also just be json since they are useless otherwise. Fix after this package is in use. - // // FIXME: is 'json' really a primitive type, or do we mean `Map` or `Map` ? - // return { type: 'json' }; - // } - // - // function objectLikeSchemaToModelType( - // nameHint: string, - // schema: jsonschema.RecordLikeObject, - // fail: Fail, - // ): Result { - // if (looksLikeBuiltinTagType(schema)) { - // return { type: 'tag' }; - // } - // - // const { typeDefinitionBuilder, freshInSession } = eventBuilder.typeDefinitionBuilder(nameHint, { schema }); - // - // // If the type has no props, it's not a RecordLikeObject and we don't need to recurse - // // @todo The type should probably also just be json since they are useless otherwise. Fix after this package is in use. - // if (freshInSession) { - // if (schema.description) { - // typeDefinitionBuilder.setFields({ documentation: schema.description }); - // } - // if (jsonschema.isRecordLikeObject(schema)) { - // recurseProperties(schema, typeDefinitionBuilder, fail.in(`typedef ${nameHint}`)); - // } - // } - // - // return { type: 'ref', reference: ref(typeDefinitionBuilder.commit()) }; - // } - // - // function looksLikeBuiltinTagType(schema: jsonschema.Object): boolean { - // if (!jsonschema.isRecordLikeObject(schema)) { - // return false; - // } - // - // const eligibleTypeNames = ['Tag', 'Tags']; - // const expectedStringProperties = ['Key', 'Value']; - // - // const resolvedProps = expectedStringProperties.map((prop) => - // schema.properties[prop] ? resolve(schema.properties[prop]) : undefined, - // ); - // - // return ( - // Object.keys(schema.properties).length === resolvedProps.length && - // resolvedProps.every((x) => x !== undefined && jsonschema.isString(x)) && - // eligibleTypeNames.includes(lastWord(jsonschema.resolvedReferenceName(schema) ?? '')) - // ); - // } - // + + /** + * Convert a JSON schema type to a type in the database model + */ + function schemaTypeToModelType( + propertyName: string, + resolvedSchema: jsonschema.ResolvedSchema, + fail: Fail, + ): Result { + return tryCatch(fail, (): Result => { + const reference = jsonschema.resolvedReference(resolvedSchema); + const referenceName = jsonschema.resolvedReferenceName(resolvedSchema); + const nameHint = referenceName ? lastWord(referenceName) : lastWord(propertyName); + + if (jsonschema.isAnyType(resolvedSchema)) { + return { type: 'json' }; + } else if (jsonschema.isOneOf(resolvedSchema) || jsonschema.isAnyOf(resolvedSchema)) { + const inner = jsonschema.innerSchemas(resolvedSchema); + // The union type is a type definition + // This is something we don't support at the moment as it's effectively a XOR for the property type + // In future we should create an enum like class for this, but we cannot at the moment to maintain backwards compatibility + // For now we assume the union is merged into a single object + if (reference && inner.every((s) => jsonschema.isObject(s))) { + report.reportFailure( + 'interpreting', + fail(`Ref ${referenceName} is a union of objects. Merging into a single type.`), + ); + const combinedType = unionSchemas(...inner) as jsonschema.ConcreteSchema; + if (isFailure(combinedType)) { + return combinedType; + } + return schemaTypeToModelType(nameHint, jsonschema.setResolvedReference(combinedType, reference), fail); + } + + // Validate oneOf and anyOf types schema by validating whether there are two definitions in oneOf/anyOf + // that has the same property name but different types. For simplicity, we do not validate if the types + // are overlapping. We will add this case to the problem report. An sample schema would be i.e. + // foo: { oneOf: [ { properties: { type: ObjectA } }, { properties: { type: ObjectB } }]} + validateCombiningSchemaType(inner, fail); + + const convertedTypes = inner.map((t) => { + if (jsonschema.isObject(t) && jsonschema.isRecordLikeObject(t)) { + // The item in union type is an object with properties + // We need to remove 'required' constraint from the object schema definition as we're dealing + // with oneOf/anyOf. Note that we should ONLY remove 'required' when the 'required' constraint + // refers to the object itself not the inner properties + const refName = jsonschema.resolvedReferenceName(t); + if ((t.title && t.required?.includes(t.title)) || (refName && t.required?.includes(refName))) { + report.reportFailure( + 'interpreting', + fail( + `${propertyName} is a union of objects. Merging into a single type and removing required fields for oneOf and anyOf.`, + ), + ); + return schemaTypeToModelType(nameHint, resolve({ ...t, required: undefined }), fail); + } + } + return schemaTypeToModelType(nameHint, resolve(t), fail); + }); + report.reportFailure('interpreting', ...convertedTypes.filter(isFailure)); + + const types = convertedTypes.filter(isSuccess); + removeUnionDuplicates(types); + + return maybeUnion(types); + } else if (jsonschema.isAllOf(resolvedSchema)) { + // FIXME: Do a proper thing here + const firstResolved = resolvedSchema.allOf[0]; + return schemaTypeToModelType(nameHint, resolve(firstResolved), fail); + } else if (jsonschema.containsRelationship(resolvedSchema)) { + // relationshipRef schema - treat as string as the type property is not present when they appear inside anyOf/oneOf + return { type: 'string' }; + } else { + switch (resolvedSchema.type) { + case 'string': + if (resolvedSchema.format === 'timestamp') { + return { type: 'date-time' }; + } + return { type: 'string' }; + + case 'array': + // FIXME: insertionOrder, uniqueItems + return using( + schemaTypeToModelType(collectionNameHint(nameHint), resolve(resolvedSchema.items ?? true), fail), + (element) => ({ + type: 'array', + element, + }), + ); + + case 'boolean': + return { type: 'boolean' }; + + case 'object': + return schemaObjectToModelType(nameHint, resolvedSchema, fail); + + case 'number': + return { type: 'number' }; + + case 'integer': + return { type: 'integer' }; + + case 'null': + return { type: 'null' }; + } + } + + throw new Error('Unable to produce type'); + }); + } + + function validateCombiningSchemaType(schema: jsonschema.ConcreteSchema[], fail: Fail) { + schema.forEach((element, index) => { + if (!jsonschema.isAnyType(element) && !jsonschema.isCombining(element)) { + schema.slice(index + 1).forEach((next) => { + if (!jsonschema.isAnyType(next) && !jsonschema.isCombining(next)) { + if (element.title === next.title && element.type !== next.type) { + report.reportFailure( + 'interpreting', + fail(`Invalid schema with property name ${element.title} but types ${element.type} and ${next.type}`), + ); + } + const elementName = jsonschema.resolvedReferenceName(element); + const nextName = jsonschema.resolvedReferenceName(next); + if (elementName && nextName && elementName === nextName && element.type !== next.type) { + report.reportFailure( + 'interpreting', + fail(`Invalid schema with property name ${elementName} but types ${element.type} and ${next.type}`), + ); + } + } + }); + } + }); + } + + function schemaObjectToModelType(nameHint: string, schema: jsonschema.Object, fail: Fail): Result { + if (jsonschema.isMapLikeObject(schema)) { + return mapLikeSchemaToModelType(nameHint, schema, fail); + } else { + return objectLikeSchemaToModelType(nameHint, schema, fail); + } + } + + function mapLikeSchemaToModelType( + nameHint: string, + schema: jsonschema.MapLikeObject, + fail: Fail, + ): Result { + const innerNameHint = collectionNameHint(nameHint); + + // Map type. If 'patternProperties' is present we'll have it take precedence, because a lot of 'additionalProperties: true' are unintentially present. + if (schema.patternProperties) { + if (schema.additionalProperties === true) { + report.reportFailure( + 'interpreting', + fail('additionalProperties: true is probably a mistake if patternProperties is also present'), + ); + } + + const unifiedPatternProps = fail.locate( + locateFailure('patternProperties')( + unionSchemas( + ...Object.values(schema.patternProperties), + // Use additionalProperties schema, but only if it's not 'true'. + ...(schema.additionalProperties && schema.additionalProperties !== true + ? [schema.additionalProperties] + : []), + ), + ), + ); + + return using(unifiedPatternProps, (unifiedType) => + using(schemaTypeToModelType(innerNameHint, resolve(unifiedType), fail), (element) => ({ + type: 'map', + element, + })), + ); + } else if (schema.additionalProperties) { + return using(schemaTypeToModelType(innerNameHint, resolve(schema.additionalProperties), fail), (element) => ({ + type: 'map', + element, + })); + } + + // Fully untyped map that's not a type + // @todo types should probably also just be json since they are useless otherwise. Fix after this package is in use. + // FIXME: is 'json' really a primitive type, or do we mean `Map` or `Map` ? + return { type: 'json' }; + } + + function objectLikeSchemaToModelType( + nameHint: string, + schema: jsonschema.RecordLikeObject, + fail: Fail, + ): Result { + if (looksLikeBuiltinTagType(schema)) { + return { type: 'tag' }; + } + + // if (eventBuilder == undefined) { + // return; + // } + // FIX: fix this bang later + const { typeDefinitionBuilder, freshInSession } = eventBuilder!.typeDefinitionBuilder(nameHint, { schema }); + + // If the type has no props, it's not a RecordLikeObject and we don't need to recurse + // @todo The type should probably also just be json since they are useless otherwise. Fix after this package is in use. + if (freshInSession) { + if (schema.description) { + typeDefinitionBuilder.setFields({ documentation: schema.description }); + } + if (jsonschema.isRecordLikeObject(schema)) { + recurseProperties(schema, typeDefinitionBuilder, fail.in(`typedef ${nameHint}`)); + } + } + + return { type: 'ref', reference: ref(typeDefinitionBuilder.commit()) }; + } + + function looksLikeBuiltinTagType(schema: jsonschema.Object): boolean { + if (!jsonschema.isRecordLikeObject(schema)) { + return false; + } + + const eligibleTypeNames = ['Tag', 'Tags']; + const expectedStringProperties = ['Key', 'Value']; + + const resolvedProps = expectedStringProperties.map((prop) => + schema.properties[prop] ? resolve(schema.properties[prop]) : undefined, + ); + + return ( + Object.keys(schema.properties).length === resolvedProps.length && + resolvedProps.every((x) => x !== undefined && jsonschema.isString(x)) && + eligibleTypeNames.includes(lastWord(jsonschema.resolvedReferenceName(schema) ?? '')) + ); + } + // function describeDefault(schema: jsonschema.ConcreteSchema): string | undefined { // if ( // jsonschema.isAnyType(schema) || @@ -387,30 +425,30 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) // /** // * Derive a 'required' array from the oneOfs/anyOfs/allOfs in this source // */ - // function calculateDefinitelyRequired(source: RequiredContainer): Set { - // const ret = new Set([...(source.required ?? [])]); - // - // if (source.oneOf) { - // setExtend(ret, setIntersect(...source.oneOf.map(calculateDefinitelyRequired))); - // } - // if (source.anyOf) { - // setExtend(ret, setIntersect(...source.anyOf.map(calculateDefinitelyRequired))); - // } - // if (source.allOf) { - // setExtend(ret, ...source.allOf.map(calculateDefinitelyRequired)); - // } - // - // return ret; - // } - // - // function withResult(x: Result, cb: (x: A) => void): void { - // if (isFailure(x)) { - // report.reportFailure('interpreting', x); - // } else { - // cb(x); - // } - // } - // + function calculateDefinitelyRequired(source: RequiredContainer): Set { + const ret = new Set([...(source.required ?? [])]); + + if (source.oneOf) { + setExtend(ret, setIntersect(...source.oneOf.map(calculateDefinitelyRequired))); + } + if (source.anyOf) { + setExtend(ret, setIntersect(...source.anyOf.map(calculateDefinitelyRequired))); + } + if (source.allOf) { + setExtend(ret, ...source.allOf.map(calculateDefinitelyRequired)); + } + + return ret; + } + + function withResult(x: Result, cb: (x: A) => void): void { + if (isFailure(x)) { + report.reportFailure('interpreting', x); + } else { + cb(x); + } + } + // function handleFailure(x: Result) { // if (isFailure(x)) { // report.reportFailure('interpreting', x); @@ -418,6 +456,70 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) // } } +// function descriptionOf(x: jsonschema.ConcreteSchema) { +// return jsonschema.isAnyType(x) ? undefined : x.description; +// } + +function lastWord(x: string): string { + return x.match(/([a-zA-Z0-9]+)$/)?.[1] ?? x; +} + +function collectionNameHint(nameHint: string) { + return `${nameHint}Items`; +} + +interface RequiredContainer { + readonly required?: string[]; + readonly oneOf?: RequiredContainer[]; + readonly anyOf?: RequiredContainer[]; + readonly allOf?: RequiredContainer[]; +} + +function setIntersect(...xs: Set[]): Set { + if (xs.length === 0) { + return new Set(); + } + const ret = new Set(xs[0]); + for (const x of xs) { + for (const e of ret) { + if (!x.has(e)) { + ret.delete(e); + } + } + } + return ret; +} + +function setExtend(ss: Set, ...xs: Set[]): void { + for (const e of xs.flatMap((x) => Array.from(x))) { + ss.add(e); + } +} +function removeUnionDuplicates(types: PropertyType[]) { + if (types.length === 0) { + throw new Error('Union cannot be empty'); + } + + for (let i = 0; i < types.length; ) { + const type = new RichPropertyType(types[i]); + + let dupe = false; + for (let j = i + 1; j < types.length; j++) { + dupe ||= type.javascriptEquals(types[j]); + } + + if (dupe) { + types.splice(i, 1); + } else { + i += 1; + } + } + + if (types.length === 0) { + throw new Error('Whoopsie, union ended up empty'); + } +} + export interface LoadEventBridgeSchmemaOptions { readonly db: SpecDatabase; readonly event: EventBridgeSchema; diff --git a/packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts b/packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts index fdbc7ee3c..2e52a14e1 100644 --- a/packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts @@ -1,4 +1,6 @@ -export interface EventBridgeSchema { +import { ImplicitJsonSchemaRecord } from '../registry-schema/CloudFormationRegistrySchema'; + +export interface EventBridgeSchema extends ImplicitJsonSchemaRecord { readonly SchemaName: string; readonly Content: { components: { diff --git a/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts b/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts index bd2e639bc..d7e448022 100644 --- a/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts @@ -359,7 +359,8 @@ export namespace jsonschema { * Make a resolver function that will resolve `$ref` entries with respect to the given document root. */ export function makeResolver(root: any) { - const resolve = (ref: Schema): ResolvedSchema => { + const resolve = (ref: Schema, weird?: boolean): ResolvedSchema => { + console.log(`weird: ${weird}`); // Don't resolve again if schema is already resolved if (isResolvedSchema(ref)) { return ref; @@ -395,7 +396,12 @@ export namespace jsonschema { } const parts = path.substring(2).split('/'); - let current = root; + let current; + if (weird == true) { + current = root.Content; + console.log('WEIRD'); + } + current = root; let lastKey: string | undefined; while (true) { if (parts.length === 0) { diff --git a/packages/@aws-cdk/service-spec-types/src/types/database.ts b/packages/@aws-cdk/service-spec-types/src/types/database.ts index 003a475c7..3a39273c4 100644 --- a/packages/@aws-cdk/service-spec-types/src/types/database.ts +++ b/packages/@aws-cdk/service-spec-types/src/types/database.ts @@ -37,6 +37,7 @@ export function emptyDatabase() { name: fieldIndex('name', stringCmp), cloudFormationNamespace: fieldIndex('cloudFormationNamespace', stringCmp), }), + // FIX: Do i really want to reuse this? I feel no typeDefinition: entityCollection(), augmentations: entityCollection(), metric: entityCollection().index({ diff --git a/packages/@aws-cdk/service-spec-types/src/types/event.ts b/packages/@aws-cdk/service-spec-types/src/types/event.ts index e042eb154..453ba71ed 100644 --- a/packages/@aws-cdk/service-spec-types/src/types/event.ts +++ b/packages/@aws-cdk/service-spec-types/src/types/event.ts @@ -16,6 +16,8 @@ export type HasEvent = Relationship; export type EventProperties = Record; export interface EventProperty { type: PropertyType; + //FIX: 99% this need to be deleted + required?: boolean; } export type EventUsesType = Relationship; From 7da94bff6ea39a31b3ffdb28a92cd0a5e00fc0b3 Mon Sep 17 00:00:00 2001 From: Gasser Date: Tue, 11 Nov 2025 10:52:19 +0100 Subject: [PATCH 03/12] fix: add the detail type, remove commented stuff that will not be used, feat: description --- .../schemas/EventBridge.schema.json | 6 +- .../src/event-builder.ts | 10 ++- .../importers/import-eventbridge-schema.ts | 74 +------------------ .../types/eventbridge/EventBridgeSchema.ts | 1 + .../src/types/registry-schema/JsonSchema.ts | 13 +++- .../service-spec-types/src/types/event.ts | 1 + 6 files changed, 23 insertions(+), 82 deletions(-) diff --git a/packages/@aws-cdk/service-spec-importers/schemas/EventBridge.schema.json b/packages/@aws-cdk/service-spec-importers/schemas/EventBridge.schema.json index 69bdbc881..6559dad1a 100644 --- a/packages/@aws-cdk/service-spec-importers/schemas/EventBridge.schema.json +++ b/packages/@aws-cdk/service-spec-importers/schemas/EventBridge.schema.json @@ -14,14 +14,12 @@ }, "Description": { "type": "string" - }, - "Type": { - "type": "string" } }, "required": [ "SchemaName", - "Content" + "Content", + "Description" ], "type": "object" }, diff --git a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts index 79add65c8..b5eeacc58 100644 --- a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts @@ -16,10 +16,11 @@ import { jsonschema } from './types'; export interface EventBuilderOptions { readonly source: string; readonly detailType: string; + readonly description: string; } export class SpecBuilder { - constructor(public readonly db: SpecDatabase) {} + constructor(public readonly db: SpecDatabase) { } public eventBuilder(schemaName: string, options: EventBuilderOptions) { const existing = this.db.lookup('event', 'name', 'equals', schemaName); @@ -72,6 +73,7 @@ export class SpecBuilder { name: schemaName.split('@').pop()!, source: options.source, detailType: options.detailType, + description: options.description, properties: { mockTypeName2: { type: { @@ -96,8 +98,8 @@ export class SpecBuilder { return new EventBuilder(this.db, event); } const resource = this.allocateResource(service); - console.log('hasEvent link is creating...'); - console.log({ resource: JSON.stringify(resource), event: JSON.stringify(event) }); + // console.log('hasEvent link is creating...'); + // console.log({ resource: JSON.stringify(resource), event: JSON.stringify(event) }); // TODO: should i return the entity only this.db.link('hasEvent', resource.entity, event); // TODO: Do i need to do this?? @@ -166,7 +168,7 @@ export class PropertyBagBuilder { protected candidateProperties: EventProperties = {}; // @ts-ignore - constructor(private readonly _propertyBag: ObjectWithProperties) {} + constructor(private readonly _propertyBag: ObjectWithProperties) { } public setProperty(name: string, prop: EventProperty) { console.log('Setting property', { prop, name }); diff --git a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts index 4114e0c61..54154279a 100644 --- a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts @@ -17,6 +17,7 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) const eventBuilder = specBuilder.eventBuilder(event.SchemaName, { source: event.Content.components.schemas.AWSEvent['x-amazon-events-source'], detailType: event.Content.components.schemas.AWSEvent['x-amazon-events-detail-type'], + description: event.Description, }); if (eventBuilder == undefined) { @@ -47,28 +48,8 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) // FIX: there's some problem here event.properties exist for some reason // @ts-ignore recurseProperties(event.Content.components.schemas.AWSEvent, eventBuilder, eventFailure); - // - // // AWS::CloudFront::ContinuousDeploymentPolicy recently introduced a change where they're marking deprecatedProperties - // // as `/definitions//properties/` instead of `/properties///`. Ignore those, as it's - // // out-of-spec - // const deprecatedProperties = (event.deprecatedProperties ?? []) - // .filter((p) => p.startsWith('/properties/')) - // .map(simplePropNameFromJsonPtr); - // eventBuilder.markDeprecatedProperties(...deprecatedProperties); - // - // // Mark everything 'readOnlyProperties` as attributes. This removes them from 'properties', as - // // in the CloudFormation registry spec, fields cannot be attributes and properties at the same time. - // const attributeNames = findAttributes(event).map(simplePropNameFromJsonPtr); - // eventBuilder.markAsAttributes(attributeNames); - // - // // Mark all 'createOnlyProperties' as immutable. - // eventBuilder.markAsImmutable((event.createOnlyProperties ?? []).map(simplePropNameFromJsonPtr)); - // // handleFailure(handleTags(eventFailure)); - // if (event.SchemaName == 'aws.a4b@RoomStateChange') { - // console.log({}); - // } return eventBuilder.commit(); // FIX: i need to pass the specific detail object not like CF schema @@ -105,37 +86,6 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) } } - /** - * Recursively extracts relationshipRef metadata from JSON schemas - * - * Finds relationshipRef objects that indicate this property references - * another CloudFormation resource, and converts property paths to name. - * This function handles the following cases: - * Single relationship for property -> { type: 'string', relationshipRef: {...} } - * When there are multiple relationships or the relationships are nested behind - * layers of oneOf/anyOf/allOf the data looks like - * { type: 'string', anyOf: [ { relationshipRef: {...} }, ...]} - */ - // function collectPossibleRelationships(schema: jsonschema.ConcreteSchema): RelationshipRef[] { - // if (jsonschema.isAnyType(schema)) { - // return []; - // } - // - // if (jsonschema.isCombining(schema)) { - // return jsonschema.innerSchemas(schema).flatMap((s) => collectPossibleRelationships(s)); - // } else if (jsonschema.isArray(schema) && schema.items) { - // return collectPossibleRelationships(resolve(schema.items)); - // } else if ('relationshipRef' in schema && jsonschema.isRelationshipRef(schema.relationshipRef)) { - // return [ - // { - // cloudFormationType: schema.relationshipRef.typeName, - // propertyName: simplePropNameFromJsonPtr(schema.relationshipRef.propertyPath), - // }, - // ]; - // } - // return []; - // } - /** * Convert a JSON schema type to a type in the database model */ @@ -374,26 +324,6 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) ); } - // function describeDefault(schema: jsonschema.ConcreteSchema): string | undefined { - // if ( - // jsonschema.isAnyType(schema) || - // jsonschema.isAllOf(schema) || - // jsonschema.isAnyOf(schema) || - // jsonschema.isOneOf(schema) - // ) { - // return undefined; - // } - // - // switch (schema.type) { - // case 'string': - // case 'number': - // case 'integer': - // case 'boolean': - // return schema.default !== undefined ? JSON.stringify(schema.default) : undefined; - // } - // - // return undefined; - // } // // function handleTags(fail: Fail) { // return tryCatch(fail, () => { @@ -500,7 +430,7 @@ function removeUnionDuplicates(types: PropertyType[]) { throw new Error('Union cannot be empty'); } - for (let i = 0; i < types.length; ) { + for (let i = 0; i < types.length;) { const type = new RichPropertyType(types[i]); let dupe = false; diff --git a/packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts b/packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts index 2e52a14e1..ebfa026df 100644 --- a/packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts @@ -2,6 +2,7 @@ import { ImplicitJsonSchemaRecord } from '../registry-schema/CloudFormationRegis export interface EventBridgeSchema extends ImplicitJsonSchemaRecord { readonly SchemaName: string; + readonly Description: string; readonly Content: { components: { schemas: { diff --git a/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts b/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts index d7e448022..c026519eb 100644 --- a/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts @@ -360,7 +360,6 @@ export namespace jsonschema { */ export function makeResolver(root: any) { const resolve = (ref: Schema, weird?: boolean): ResolvedSchema => { - console.log(`weird: ${weird}`); // Don't resolve again if schema is already resolved if (isResolvedSchema(ref)) { return ref; @@ -400,8 +399,12 @@ export namespace jsonschema { if (weird == true) { current = root.Content; console.log('WEIRD'); + console.log({ path, typeof: typeof root, root: JSON.stringify(root), current: JSON.stringify(current) }); + } else { + current = root; + // console.log('NOTWIERD'); + // console.log({ typeof: typeof root, root: JSON.stringify(root), current: JSON.stringify(root) }); } - current = root; let lastKey: string | undefined; while (true) { if (parts.length === 0) { @@ -410,6 +413,12 @@ export namespace jsonschema { lastKey = parts.shift()!; current = current[lastKey]; } + + if (weird == true) { + console.log('WEIRD IN another place'); + console.log({ current: JSON.stringify(current) }); + } + if (current === undefined) { throw new Error(`Invalid $ref: ${path}`); } diff --git a/packages/@aws-cdk/service-spec-types/src/types/event.ts b/packages/@aws-cdk/service-spec-types/src/types/event.ts index 453ba71ed..fdf346c80 100644 --- a/packages/@aws-cdk/service-spec-types/src/types/event.ts +++ b/packages/@aws-cdk/service-spec-types/src/types/event.ts @@ -3,6 +3,7 @@ import { PropertyType, Resource, TypeDefinition } from './resource'; export interface Event extends Entity { readonly name: string; + readonly description: string; readonly source: string; readonly detailType: string; readonly identifiersPath: Array; From 2a187b945857cb1522be04cbda40d590684c47b9 Mon Sep 17 00:00:00 2001 From: Gasser Date: Tue, 11 Nov 2025 13:33:53 +0100 Subject: [PATCH 04/12] feat: move event types to eventTypeDefintion --- .../src/event-builder.ts | 62 +++++++++---------- .../importers/import-eventbridge-schema.ts | 16 ++--- .../service-spec-types/src/types/database.ts | 6 +- .../service-spec-types/src/types/event.ts | 14 ++++- .../service-spec-types/src/types/resource.ts | 3 +- 5 files changed, 57 insertions(+), 44 deletions(-) diff --git a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts index b5eeacc58..ee2595bb3 100644 --- a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts @@ -5,8 +5,8 @@ import { RichProperty, Service, SpecDatabase, - TypeDefinition, Event, + EventTypeDefinition, } from '@aws-cdk/service-spec-types'; import { Entity, Reference } from '@cdklabs/tskb'; import { AllFieldsGiven } from './diff-helpers'; @@ -20,7 +20,7 @@ export interface EventBuilderOptions { } export class SpecBuilder { - constructor(public readonly db: SpecDatabase) { } + constructor(public readonly db: SpecDatabase) {} public eventBuilder(schemaName: string, options: EventBuilderOptions) { const existing = this.db.lookup('event', 'name', 'equals', schemaName); @@ -41,7 +41,7 @@ export class SpecBuilder { // FIX: mocking a type just for not blocking the code generation, need to be removed - const typeDef = this.db.allocate('typeDefinition', { + const typeDef = this.db.allocate('eventTypeDefinition', { name: 'mockTypeName', properties: { mockFieldName: { @@ -56,7 +56,7 @@ export class SpecBuilder { return typeof x === 'string' ? { $ref: x } : { $ref: x.$id }; } - const typeDef2 = this.db.allocate('typeDefinition', { + const typeDef2 = this.db.allocate('eventTypeDefinition', { name: 'mockTypeName2', properties: { mockFieldName: { @@ -168,7 +168,7 @@ export class PropertyBagBuilder { protected candidateProperties: EventProperties = {}; // @ts-ignore - constructor(private readonly _propertyBag: ObjectWithProperties) { } + constructor(private readonly _propertyBag: ObjectWithProperties) {} public setProperty(name: string, prop: EventProperty) { console.log('Setting property', { prop, name }); @@ -245,14 +245,14 @@ export class PropertyBagBuilder { if (!prop.required) { delete prop.required; } - if (prop.causesReplacement === 'no') { - delete prop.causesReplacement; - } + // if (prop.causesReplacement === 'no') { + // delete prop.causesReplacement; + // } } } export class EventBuilder extends PropertyBagBuilder { - private typeDefinitions = new Map(); + private eventTypeDefinitions = new Map(); private typesCreatedHere = new Set(); // // /** @@ -437,47 +437,46 @@ export class EventBuilder extends PropertyBagBuilder { // return currentBag[fieldPath[fieldPath.length - 1]]; // } // - public typeDefinitionBuilder( + public eventTypeDefinitionBuilder( typeName: string, options?: { description?: string; schema?: jsonschema.RecordLikeObject }, ) { - const existing = this.typeDefinitions.get(typeName); - const description = options?.description; + const existing = this.eventTypeDefinitions.get(typeName); + // const description = options?.description; const freshInSession = !this.typesCreatedHere.has(typeName); this.typesCreatedHere.add(typeName); if (existing) { - if (!existing.documentation && description) { - existing.documentation = description; - } + // if (!existing.documentation && description) { + // existing.documentation = description; + // } const properties = options?.schema?.properties ?? {}; // If db already contains typeName's type definition, we want to additionally // check if the schema matches the type definition. If the schema includes new // properties, we want to add them to the type definition. if (!Object.keys(properties).every((element) => Object.keys(existing.properties).includes(element))) { return { - typeDefinitionBuilder: new TypeDefinitionBuilder(this.db, existing), + eventTypeDefinitionBuilder: new EventTypeDefinitionBuilder(this.db, existing), freshInDb: true, freshInSession: true, }; } return { - typeDefinitionBuilder: new TypeDefinitionBuilder(this.db, existing), + eventTypeDefinitionBuilder: new EventTypeDefinitionBuilder(this.db, existing), freshInDb: false, freshInSession, }; } - const typeDef = this.db.allocate('typeDefinition', { + const typeDef = this.db.allocate('eventTypeDefinition', { name: typeName, - documentation: description, properties: {}, }); this.db.link('eventUsesType', this.event, typeDef); - this.typeDefinitions.set(typeName, typeDef); + this.eventTypeDefinitions.set(typeName, typeDef); - const builder = new TypeDefinitionBuilder(this.db, typeDef); - return { typeDefinitionBuilder: builder, freshInDb: true, freshInSession }; + const builder = new EventTypeDefinitionBuilder(this.db, typeDef); + return { eventTypeDefinitionBuilder: builder, freshInDb: true, freshInSession }; } /** @@ -513,22 +512,23 @@ export class EventBuilder extends PropertyBagBuilder { // } } -export type TypeDefinitionFields = Pick; +// export type EventTypeDefinitionFields = Pick; -export class TypeDefinitionBuilder extends PropertyBagBuilder { - private readonly fields: TypeDefinitionFields = {}; +export class EventTypeDefinitionBuilder extends PropertyBagBuilder { + // private readonly fields: EventTypeDefinitionFields = {}; - constructor(public readonly db: SpecDatabase, private readonly typeDef: TypeDefinition) { + // @ts-ignore + constructor(public readonly db: SpecDatabase, private readonly typeDef: EventTypeDefinition) { super(typeDef); } - public setFields(fields: TypeDefinitionFields) { - Object.assign(this.fields, fields); - } + // public setFields(fields: EventTypeDefinitionFields) { + // Object.assign(this.fields, fields); + // } - public commit(): TypeDefinition { + public commit(): EventTypeDefinition { super.commit(); - Object.assign(this.typeDef, this.fields); + // Object.assign(this.typeDef, this.fields); return this.typeDef; } } diff --git a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts index 54154279a..849034790 100644 --- a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts @@ -289,20 +289,22 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) // return; // } // FIX: fix this bang later - const { typeDefinitionBuilder, freshInSession } = eventBuilder!.typeDefinitionBuilder(nameHint, { schema }); + const { eventTypeDefinitionBuilder, freshInSession } = eventBuilder!.eventTypeDefinitionBuilder(nameHint, { + schema, + }); // If the type has no props, it's not a RecordLikeObject and we don't need to recurse // @todo The type should probably also just be json since they are useless otherwise. Fix after this package is in use. if (freshInSession) { - if (schema.description) { - typeDefinitionBuilder.setFields({ documentation: schema.description }); - } + // if (schema.description) { + // eventTypeDefinitionBuilder.setFields({ documentation: schema.description }); + // } if (jsonschema.isRecordLikeObject(schema)) { - recurseProperties(schema, typeDefinitionBuilder, fail.in(`typedef ${nameHint}`)); + recurseProperties(schema, eventTypeDefinitionBuilder, fail.in(`typedef ${nameHint}`)); } } - return { type: 'ref', reference: ref(typeDefinitionBuilder.commit()) }; + return { type: 'ref', reference: ref(eventTypeDefinitionBuilder.commit()) }; } function looksLikeBuiltinTagType(schema: jsonschema.Object): boolean { @@ -430,7 +432,7 @@ function removeUnionDuplicates(types: PropertyType[]) { throw new Error('Union cannot be empty'); } - for (let i = 0; i < types.length;) { + for (let i = 0; i < types.length; ) { const type = new RichPropertyType(types[i]); let dupe = false; diff --git a/packages/@aws-cdk/service-spec-types/src/types/database.ts b/packages/@aws-cdk/service-spec-types/src/types/database.ts index 3a39273c4..e58991c1f 100644 --- a/packages/@aws-cdk/service-spec-types/src/types/database.ts +++ b/packages/@aws-cdk/service-spec-types/src/types/database.ts @@ -2,7 +2,7 @@ import { promises as fs } from 'fs'; import { gunzipSync } from 'zlib'; import { Database, entityCollection, fieldIndex, stringCmp } from '@cdklabs/tskb'; import { IsAugmentedResource, ResourceAugmentation } from './augmentations'; -import { HasEvent, Event, EventUsesType } from './event'; +import { HasEvent, Event, EventUsesType, EventTypeDefinition } from './event'; import { DimensionSet, Metric, @@ -37,7 +37,7 @@ export function emptyDatabase() { name: fieldIndex('name', stringCmp), cloudFormationNamespace: fieldIndex('cloudFormationNamespace', stringCmp), }), - // FIX: Do i really want to reuse this? I feel no + eventTypeDefinition: entityCollection(), typeDefinition: entityCollection(), augmentations: entityCollection(), metric: entityCollection().index({ @@ -64,7 +64,7 @@ export function emptyDatabase() { resourceHasDimensionSet: r.relationship('resource', 'dimensionSet'), serviceHasDimensionSet: r.relationship('service', 'dimensionSet'), hasEvent: r.relationship('resource', 'event'), - eventUsesType: r.relationship('event', 'typeDefinition'), + eventUsesType: r.relationship('event', 'eventTypeDefinition'), }), ); } diff --git a/packages/@aws-cdk/service-spec-types/src/types/event.ts b/packages/@aws-cdk/service-spec-types/src/types/event.ts index fdf346c80..9cebfc4d6 100644 --- a/packages/@aws-cdk/service-spec-types/src/types/event.ts +++ b/packages/@aws-cdk/service-spec-types/src/types/event.ts @@ -1,5 +1,5 @@ import { Entity, Relationship } from '@cdklabs/tskb'; -import { PropertyType, Resource, TypeDefinition } from './resource'; +import { PropertyType, Resource } from './resource'; export interface Event extends Entity { readonly name: string; @@ -21,4 +21,14 @@ export interface EventProperty { required?: boolean; } -export type EventUsesType = Relationship; +export interface EventTypeDefinition extends Entity { + readonly name: string; + readonly properties: EventProperties; +} + +// export interface EventDefinitionReference { +// readonly type: 'ref'; +// readonly reference: Reference; +// } + +export type EventUsesType = Relationship; diff --git a/packages/@aws-cdk/service-spec-types/src/types/resource.ts b/packages/@aws-cdk/service-spec-types/src/types/resource.ts index 02426219d..11c15f8c3 100644 --- a/packages/@aws-cdk/service-spec-types/src/types/resource.ts +++ b/packages/@aws-cdk/service-spec-types/src/types/resource.ts @@ -1,5 +1,6 @@ import { Entity, Reference, Relationship } from '@cdklabs/tskb'; import { SpecDatabase } from './database'; +import { EventTypeDefinition } from './event'; import { sortKeyComparator } from '../util/sorting'; export interface Partition extends Entity { @@ -348,7 +349,7 @@ export interface BuiltinTagType { export interface DefinitionReference { readonly type: 'ref'; - readonly reference: Reference; + readonly reference: Reference; } export interface ArrayType { From a78ba5fbe7d9bf84bbf21ead6aa303cea59ba862 Mon Sep 17 00:00:00 2001 From: Gasser Date: Wed, 12 Nov 2025 10:25:54 +0100 Subject: [PATCH 05/12] fix: now only detail is in the property with the right naming type --- .../importers/import-eventbridge-schema.ts | 74 +++++++++++++++---- 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts index 849034790..735ccb1cc 100644 --- a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts @@ -6,6 +6,14 @@ import { unionSchemas } from '../schema-manipulation/unify-schemas'; import { maybeUnion } from '../type-manipulation'; import { EventBridgeSchema, ImplicitJsonSchemaRecord, jsonschema } from '../types'; +/** + * Check if a schema represents an empty object type + * An empty object type has type: "object" but no properties field + */ +function isEmptyObjectType(schema: any): boolean { + return schema.type === 'object' && !schema.properties; +} + export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) { const { db, event } = options; // FIX: this pointing toward CF resource @@ -33,21 +41,61 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) // FIX: jsonschema pointing toward cloudformation thing const resolve = jsonschema.makeResolver(event); - // const parts = event.Content.components.schemas.AWSEvent.properties.detail.$ref.substring(2).split('/'); - // let current = event.Content; - // let lastKey: string | undefined; - // while (true) { - // if (parts.length === 0) { - // break; - // } - // lastKey = parts.shift()!; - // // @ts-ignore - // current = current[lastKey]; - // } + const parts = event.Content.components.schemas.AWSEvent.properties.detail.$ref.substring(2).split('/'); + let current = event.Content; + let lastKey: string | undefined; + while (true) { + if (parts.length === 0) { + break; + } + lastKey = parts.shift()!; + // @ts-ignore + current = current[lastKey]; + } - // FIX: there's some problem here event.properties exist for some reason + // Get the type name from the reference (e.g., "ScheduledEvent") + const detailTypeName = lastKey; + + // Determine if detail is required // @ts-ignore - recurseProperties(event.Content.components.schemas.AWSEvent, eventBuilder, eventFailure); + const required2 = event.Content.components.schemas.AWSEvent.required?.includes('detail') ?? false; + + // Check if the resolved detail type is an empty object + // @ts-ignore - current is dynamically resolved from the schema + if (isEmptyObjectType(current)) { + // Treat as JSON type - add a property with the detail type name + eventBuilder.setProperty(detailTypeName!, { + type: { type: 'json' }, + required: required2, + }); + // @ts-ignore - current is dynamically resolved from the schema + } else if (current.properties) { + // Create a type definition for the detail type + const { eventTypeDefinitionBuilder } = eventBuilder.eventTypeDefinitionBuilder(detailTypeName!, { + // @ts-ignore - current is dynamically resolved from the schema + schema: current, + }); + + // Recurse into the detail type's properties to build the type definition + // @ts-ignore - current is dynamically resolved from the schema + recurseProperties(current, eventTypeDefinitionBuilder, eventFailure); + + // Commit the type definition to get a reference + const typeDef = eventTypeDefinitionBuilder.commit(); + + // Add a property to the event that references the type definition + eventBuilder.setProperty(detailTypeName!, { + type: { type: 'ref', reference: ref(typeDef) }, + required: required2, + }); + } else { + // Unexpected case: not an object with properties and not an empty object + report.reportFailure( + 'interpreting', + eventFailure(`Detail type has unexpected structure: ${JSON.stringify(current)}`), + ); + } + // recurseProperties(event.Content.components.schemas.AWSEvent, eventBuilder, eventFailure); // handleFailure(handleTags(eventFailure)); return eventBuilder.commit(); From 77b53e94a0fde29abca94b77967c3a6a3b100a79 Mon Sep 17 00:00:00 2001 From: Gasser Date: Wed, 12 Nov 2025 10:27:43 +0100 Subject: [PATCH 06/12] fix: remove the mocks type from the db --- .../src/event-builder.ts | 68 ++++++++----------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts index ee2595bb3..98dd0d1e4 100644 --- a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts @@ -8,7 +8,6 @@ import { Event, EventTypeDefinition, } from '@aws-cdk/service-spec-types'; -import { Entity, Reference } from '@cdklabs/tskb'; import { AllFieldsGiven } from './diff-helpers'; import { jsonschema } from './types'; @@ -20,7 +19,7 @@ export interface EventBuilderOptions { } export class SpecBuilder { - constructor(public readonly db: SpecDatabase) {} + constructor(public readonly db: SpecDatabase) { } public eventBuilder(schemaName: string, options: EventBuilderOptions) { const existing = this.db.lookup('event', 'name', 'equals', schemaName); @@ -41,53 +40,46 @@ export class SpecBuilder { // FIX: mocking a type just for not blocking the code generation, need to be removed - const typeDef = this.db.allocate('eventTypeDefinition', { - name: 'mockTypeName', - properties: { - mockFieldName: { - type: { - type: 'string', - }, - }, - }, - }); + // const typeDef = this.db.allocate('eventTypeDefinition', { + // name: 'mockTypeName', + // properties: { + // mockFieldName: { + // type: { + // type: 'string', + // }, + // }, + // }, + // }); - function ref(x: E | string): Reference { - return typeof x === 'string' ? { $ref: x } : { $ref: x.$id }; - } + // function ref(x: E | string): Reference { + // return typeof x === 'string' ? { $ref: x } : { $ref: x.$id }; + // } - const typeDef2 = this.db.allocate('eventTypeDefinition', { - name: 'mockTypeName2', - properties: { - mockFieldName: { - type: { - type: 'ref', - reference: ref(typeDef), - }, - }, - }, - }); - typeDef.name; + // const typeDef2 = this.db.allocate('eventTypeDefinition', { + // name: 'mockTypeName2', + // properties: { + // mockFieldName: { + // type: { + // type: 'ref', + // reference: ref(typeDef), + // }, + // }, + // }, + // }); + // typeDef.name; const event = this.db.allocate('event', { // FIX: need to fix the bang? name: schemaName.split('@').pop()!, source: options.source, detailType: options.detailType, description: options.description, - properties: { - mockTypeName2: { - type: { - type: 'ref', - reference: ref(typeDef2), - }, - }, - }, + properties: {}, identifiersPath: ['mockTypeName2.mockTypeName'], // attributes: {}, }); - this.db.link('eventUsesType', event, typeDef); - this.db.link('eventUsesType', event, typeDef2); + // this.db.link('eventUsesType', event, typeDef); + // this.db.link('eventUsesType', event, typeDef2); // mocking type ends // // TODO: add more information for the event @@ -168,7 +160,7 @@ export class PropertyBagBuilder { protected candidateProperties: EventProperties = {}; // @ts-ignore - constructor(private readonly _propertyBag: ObjectWithProperties) {} + constructor(private readonly _propertyBag: ObjectWithProperties) { } public setProperty(name: string, prop: EventProperty) { console.log('Setting property', { prop, name }); From e944ca0e56fadb991bc9a913908ef91b1915dc45 Mon Sep 17 00:00:00 2001 From: Gasser Date: Wed, 12 Nov 2025 15:18:45 +0100 Subject: [PATCH 07/12] fix: use the event property type instead of the resource ones --- .../src/event-builder.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts index 98dd0d1e4..86e50ae84 100644 --- a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts @@ -1,7 +1,7 @@ import { EventProperties, EventProperty, - Property, + // Property, RichProperty, Service, SpecDatabase, @@ -188,7 +188,7 @@ export class PropertyBagBuilder { return this._propertyBag; } - private commitProperty(name: string, prop: Property) { + private commitProperty(name: string, prop: EventProperty) { if (this._propertyBag.properties[name]) { this.mergeProperty(this._propertyBag.properties[name], prop); } else { @@ -197,31 +197,31 @@ export class PropertyBagBuilder { this.simplifyProperty(this._propertyBag.properties[name]); } - protected mergeProperty(prop: Property, updates: Property) { + protected mergeProperty(prop: EventProperty, updates: EventProperty) { // This handles merges that are trivial scalar overwrites. All // fields must be represented, if you have special code to handle // a field, put it in here as 'undefined' and add code to handle it below. copyDefined({ - causesReplacement: updates.causesReplacement, - defaultValue: updates.defaultValue, - deprecated: updates.deprecated, - documentation: updates.documentation, + // causesReplacement: updates.causesReplacement, + // defaultValue: updates.defaultValue, + // deprecated: updates.deprecated, + // documentation: updates.documentation, required: updates.required, - scrutinizable: updates.scrutinizable, - relationshipRefs: updates.relationshipRefs, + // scrutinizable: updates.scrutinizable, + // relationshipRefs: updates.relationshipRefs, // These will be handled specially below - previousTypes: undefined, + // previousTypes: undefined, type: undefined, }); // Special field handling - for (const type of updates.previousTypes ?? []) { - new RichProperty(prop).updateType(type); - } + // for (const type of updates.previousTypes ?? []) { + // new RichProperty(prop).updateType(type); + // } new RichProperty(prop).updateType(updates.type); - function copyDefined(upds: AllFieldsGiven>) { + function copyDefined(upds: AllFieldsGiven>) { for (const [key, value] of Object.entries(upds)) { if (value !== undefined) { (prop as any)[key] = value; @@ -233,7 +233,7 @@ export class PropertyBagBuilder { /** * Remove settings that are equal to their defaults */ - protected simplifyProperty(prop: Property) { + protected simplifyProperty(prop: EventProperty) { if (!prop.required) { delete prop.required; } From 393c10bd186838f28a1a5305fd302154ecf001da Mon Sep 17 00:00:00 2001 From: Gasser Date: Wed, 12 Nov 2025 15:47:12 +0100 Subject: [PATCH 08/12] add some logs --- .../@aws-cdk/service-spec-importers/src/db-builder.ts | 2 ++ .../@aws-cdk/service-spec-importers/src/event-builder.ts | 4 ++-- .../src/importers/import-eventbridge-schema.ts | 8 ++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/service-spec-importers/src/db-builder.ts b/packages/@aws-cdk/service-spec-importers/src/db-builder.ts index 5fd03fc6c..8a60e721b 100644 --- a/packages/@aws-cdk/service-spec-importers/src/db-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/db-builder.ts @@ -231,6 +231,8 @@ export class DatabaseBuilder { report, region: region.regionName, }); + const existing = db.lookup('event', 'name', 'equals', event.SchemaName.split('@')[1]); + console.log('db-builder', { existing }); } } }); diff --git a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts index 86e50ae84..b5e5ca416 100644 --- a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts @@ -19,7 +19,7 @@ export interface EventBuilderOptions { } export class SpecBuilder { - constructor(public readonly db: SpecDatabase) { } + constructor(public readonly db: SpecDatabase) {} public eventBuilder(schemaName: string, options: EventBuilderOptions) { const existing = this.db.lookup('event', 'name', 'equals', schemaName); @@ -160,7 +160,7 @@ export class PropertyBagBuilder { protected candidateProperties: EventProperties = {}; // @ts-ignore - constructor(private readonly _propertyBag: ObjectWithProperties) { } + constructor(private readonly _propertyBag: ObjectWithProperties) {} public setProperty(name: string, prop: EventProperty) { console.log('Setting property', { prop, name }); diff --git a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts index 735ccb1cc..e21add356 100644 --- a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts @@ -98,7 +98,11 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) // recurseProperties(event.Content.components.schemas.AWSEvent, eventBuilder, eventFailure); // handleFailure(handleTags(eventFailure)); - return eventBuilder.commit(); + const existing = db.lookup('event', 'name', 'equals', event.SchemaName.split('@')[1]); + console.log('in import', { existing }); + const eventRet = eventBuilder.commit(); + + return eventRet; // FIX: i need to pass the specific detail object not like CF schema function recurseProperties(source: ImplicitJsonSchemaRecord, target: PropertyBagBuilder, fail: Fail) { @@ -480,7 +484,7 @@ function removeUnionDuplicates(types: PropertyType[]) { throw new Error('Union cannot be empty'); } - for (let i = 0; i < types.length; ) { + for (let i = 0; i < types.length;) { const type = new RichPropertyType(types[i]); let dupe = false; From a7e89a95b8fc91cec5b69e354a9400291a7559bf Mon Sep 17 00:00:00 2001 From: Gasser Date: Wed, 12 Nov 2025 16:28:07 +0100 Subject: [PATCH 09/12] refactor: move the decider after the types generated --- .../src/event-builder.ts | 61 +--------------- .../importers/import-eventbridge-schema.ts | 72 ++++++++++++++++++- 2 files changed, 70 insertions(+), 63 deletions(-) diff --git a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts index b5e5ca416..e77c74e0b 100644 --- a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts @@ -1,9 +1,7 @@ import { EventProperties, EventProperty, - // Property, RichProperty, - Service, SpecDatabase, Event, EventTypeDefinition, @@ -69,7 +67,7 @@ export class SpecBuilder { // typeDef.name; const event = this.db.allocate('event', { // FIX: need to fix the bang? - name: schemaName.split('@').pop()!, + name: schemaName, source: options.source, detailType: options.detailType, description: options.description, @@ -84,16 +82,6 @@ export class SpecBuilder { // // TODO: add more information for the event - const service = this.allocateService(schemaName); - if (service == undefined) { - // TODO: Maybe i need to return undefined - return new EventBuilder(this.db, event); - } - const resource = this.allocateResource(service); - // console.log('hasEvent link is creating...'); - // console.log({ resource: JSON.stringify(resource), event: JSON.stringify(event) }); - // TODO: should i return the entity only - this.db.link('hasEvent', resource.entity, event); // TODO: Do i need to do this?? // if (options.region) { // const region = this.allocateRegion(options.region); @@ -103,53 +91,6 @@ export class SpecBuilder { return new EventBuilder(this.db, event); } - - // TODO: change name to get? - private allocateService(eventSchemaName: string, eventTypeNameSeparator = '@') { - const parts = eventSchemaName.split(eventTypeNameSeparator); - // parts e.g. ["aws.s3", "ObjectCreated"] - const serviceName = parts[0].replace('.', '-').toLowerCase(); - - const services = this.db.lookup('service', 'name', 'equals', serviceName); - - if (services.length == 0) { - return; - } - - // TODO: i think only will do that for me - // if (true) { - // throw Error(`This service doesn't existing in cloudformation ${serviceName}`); - // } - return services.only(); - } - - // TODO: change name to get? - private allocateResource(service: Service) { - const resource = this.eventDecider(service); - - return resource; - // TODO: I have no idea what i'm doing now :D, how the resource will not be in the DB? - // const resource = this.db.allocate('service', { - // name, - // shortName, - // capitalized, - // cloudFormationNamespace, - // }); - - // return resource; - } - - // TODO: change name to resource decider? - private eventDecider(service: Service) { - // TODO: need to get all the requred property field names here - const resources = this.db.follow('hasResource', service); - if (service.name == 'aws-lambda') { - console.log({ resources: JSON.stringify(resources, null, 2) }); - } - - // TODO: Change this to proper resource - return resources[0]; - } } // interface ObjectWithProperties { diff --git a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts index e21add356..7844bced5 100644 --- a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts @@ -1,4 +1,4 @@ -import { PropertyType, RichPropertyType, SpecDatabase } from '@aws-cdk/service-spec-types'; +import { PropertyType, RichPropertyType, Service, SpecDatabase } from '@aws-cdk/service-spec-types'; import { locateFailure, Fail, failure, isFailure, Result, tryCatch, using, ref, isSuccess } from '@cdklabs/tskb'; import { SpecBuilder, PropertyBagBuilder } from '../event-builder'; import { ProblemReport, ReportAudience } from '../report'; @@ -14,6 +14,62 @@ function isEmptyObjectType(schema: any): boolean { return schema.type === 'object' && !schema.properties; } +// TODO: change name to get? +function allocateService({ + eventSchemaName, + eventTypeNameSeparator = '@', + db, +}: { + eventSchemaName: string; + eventTypeNameSeparator?: string; + db: SpecDatabase; +}): Service | undefined { + const schemaNameParts = eventSchemaName.split(eventTypeNameSeparator); + // parts e.g. ["aws.s3", "ObjectCreated"] + const serviceName = schemaNameParts[0].replace('.', '-').toLowerCase(); + + const services = db.lookup('service', 'name', 'equals', serviceName); + + if (services.length == 0) { + return; + } + + // TODO: i think only will do that for me + // if (true) { + // throw Error(`This service doesn't existing in cloudformation ${serviceName}`); + // } + return services.only(); +} + +// TODO: change name to get? +function allocateResource({ db, service }: { db: SpecDatabase; service: Service }) { + const resource = eventDecider({ service, db }); + + return resource; + // TODO: I have no idea what i'm doing now :D, how the resource will not be in the DB? + // const resource = this.db.allocate('service', { + // name, + // shortName, + // capitalized, + // cloudFormationNamespace, + // }); + + // return resource; +} + +// TODO: change name to resource decider? +// function eventDecider(service: Service) { +function eventDecider({ db, service }: { db: SpecDatabase; service: Service }) { + // TODO: need to get all the requred property field names here + const resources = db.follow('hasResource', service); + if (service.name == 'aws-lambda') { + console.log({ resources: JSON.stringify(resources, null, 2) }); + } + + // TODO: Change this to proper resource + return resources[0]; +} + export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) { const { db, event } = options; // FIX: this pointing toward CF resource @@ -98,10 +154,20 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) // recurseProperties(event.Content.components.schemas.AWSEvent, eventBuilder, eventFailure); // handleFailure(handleTags(eventFailure)); - const existing = db.lookup('event', 'name', 'equals', event.SchemaName.split('@')[1]); - console.log('in import', { existing }); const eventRet = eventBuilder.commit(); + const service = allocateService({ eventSchemaName: event.SchemaName, db }); + if (service == undefined) { + // TODO: Maybe i need to return undefined + return eventRet; + } + const resource = allocateResource({ service, db }); + // console.log('hasEvent link is creating...'); + // console.log({ resource: JSON.stringify(resource), event: JSON.stringify(event) }); + + const eventDB = db.lookup('event', 'name', 'equals', event.SchemaName).only(); + // TODO: should i return the entity only + db.link('hasEvent', resource.entity, eventDB); return eventRet; // FIX: i need to pass the specific detail object not like CF schema From 16d0239356e22e6e59cdadae5cebcbc29be2a064 Mon Sep 17 00:00:00 2001 From: Gasser Date: Thu, 13 Nov 2025 10:11:06 +0100 Subject: [PATCH 10/12] feat: add event to the actual CF resource --- .../service-spec-importers/src/db-builder.ts | 6 +- .../src/event-builder.ts | 2 +- .../importers/import-eventbridge-schema.ts | 259 ++++++++++++++++-- .../src/types/registry-schema/JsonSchema.ts | 8 +- 4 files changed, 250 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/service-spec-importers/src/db-builder.ts b/packages/@aws-cdk/service-spec-importers/src/db-builder.ts index 8a60e721b..1a4cc97c5 100644 --- a/packages/@aws-cdk/service-spec-importers/src/db-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/db-builder.ts @@ -224,15 +224,15 @@ export class DatabaseBuilder { }); for (const region of regions) { for (const event of region.events) { - console.log({ region, resource: JSON.stringify(event, null, 2), name: event.SchemaName }); + // console.log({ region, resource: JSON.stringify(event, null, 2), name: event.SchemaName }); importEventBridgeSchema({ db, event, report, region: region.regionName, }); - const existing = db.lookup('event', 'name', 'equals', event.SchemaName.split('@')[1]); - console.log('db-builder', { existing }); + // const existing = db.lookup('event', 'name', 'equals', event.SchemaName.split('@')[1]); + // console.log('db-builder', { existing }); } } }); diff --git a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts index e77c74e0b..6f8b84287 100644 --- a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts @@ -104,7 +104,7 @@ export class PropertyBagBuilder { constructor(private readonly _propertyBag: ObjectWithProperties) {} public setProperty(name: string, prop: EventProperty) { - console.log('Setting property', { prop, name }); + // console.log('Setting property', { prop, name }); this.candidateProperties[name] = prop; } // diff --git a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts index 7844bced5..96535aeb8 100644 --- a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts @@ -1,5 +1,5 @@ -import { PropertyType, RichPropertyType, Service, SpecDatabase } from '@aws-cdk/service-spec-types'; -import { locateFailure, Fail, failure, isFailure, Result, tryCatch, using, ref, isSuccess } from '@cdklabs/tskb'; +import { PropertyType, RichPropertyType, Service, SpecDatabase, Event, Resource } from '@aws-cdk/service-spec-types'; +import { locateFailure, Fail, failure, isFailure, Result, tryCatch, using, ref, isSuccess, Link } from '@cdklabs/tskb'; import { SpecBuilder, PropertyBagBuilder } from '../event-builder'; import { ProblemReport, ReportAudience } from '../report'; import { unionSchemas } from '../schema-manipulation/unify-schemas'; @@ -42,8 +42,16 @@ function allocateService({ } // TODO: change name to get? -function allocateResource({ db, service }: { db: SpecDatabase; service: Service }) { - const resource = eventDecider({ service, db }); +function allocateResource({ + db, + service, + event, +}: { + db: SpecDatabase; + service: Service; + event: Event; +}): Link | undefined { + const resource = eventDecider({ service, db, event }); return resource; // TODO: I have no idea what i'm doing now :D, how the resource will not be in the DB? @@ -57,17 +65,223 @@ function allocateResource({ db, service }: { db: SpecDatabase; service: Service // return resource; } +/** + * TypeInfo interface for event type definitions + */ +interface TypeInfo { + typeId: string; + typeName: string; + fields: string[]; +} + +/** + * MatchDetail interface for tracking what matched + */ +interface MatchDetail { + typeId: string; + fieldName?: string; +} + +/** + * ResourceMatch interface for resources with matches + */ +interface ResourceMatch { + resource: Link; + matches: MatchDetail[]; +} + +/** + * Normalize a name into lowercase segments for comparison + * Splits by hyphens, underscores, and camelCase + * Filters out generic identifiers (name, id, arn) + */ +function normalizeNameToSegments(name: string): string[] { + // Split camelCase: insert hyphen before capitals + const withHyphens = name.replace(/([A-Z])/g, '-$1'); + + // Split by hyphens and underscores + const segments = withHyphens.toLowerCase().split(/[-_]/); + + // Filter out empty strings and generic identifiers + const genericIds = new Set(['name', 'id', 'arn']); + return segments.filter((s) => s.length > 0 && !genericIds.has(s)); +} + +/** + * Extract type information from an event + * Queries db.follow('eventUsesType', event) to get all type definitions + * Returns array of TypeInfo objects with typeId, typeName, and fields + */ +function extractEventTypeInfo(db: SpecDatabase, event: Event): TypeInfo[] { + // Query for type definitions linked to the event + const typeDefinitions = db.follow('eventUsesType', event); + + // If no type definitions found, return empty array + if (!typeDefinitions || typeDefinitions.length === 0) { + return []; + } + + // Extract type information from each type definition + const typeInfos: TypeInfo[] = []; + for (const typeDefWrapper of typeDefinitions) { + const typeDef = typeDefWrapper.entity; + const typeId = typeDef.$id; + const typeName = typeDef.name || ''; + const fields = typeDef.properties ? Object.keys(typeDef.properties) : []; + + typeInfos.push({ + typeId, + typeName, + fields, + }); + } + + return typeInfos; +} + +/** + * Match event types and fields to CloudFormation resources + * Compares normalized type names and field names against resource type names + * Returns array of ResourceMatch objects for resources with at least one match + */ +function matchTypesAndFieldsToResources(resources: Link[], typeInfos: TypeInfo[]): ResourceMatch[] { + const resourceMatches: ResourceMatch[] = []; + + // Iterate through all resources from service + for (const resourceWrapper of resources) { + const resource = resourceWrapper.entity; + const matches: MatchDetail[] = []; + + // Extract resource type name from CloudFormation type + // e.g., "Bucket" from "AWS::S3::Bucket" + const cfType = resource.cloudFormationType || ''; + const parts = cfType.split('::'); + const resourceTypeName = parts.length >= 3 ? parts[2] : cfType; + + // Normalize resource type name using normalizeNameToSegments + // TODO: i don't think this is needed + // const resourceSegments = normalizeNameToSegments(resourceTypeName); + // FIX: return the below + const resourceSegments = resourceTypeName; + console.log('Match Types & Fields To Resources', { + cfType, + resourceTypeName, + resourceSegments: JSON.stringify(resourceSegments, null, 2), + }); + + // For each type info, compare against resource + for (const typeInfo of typeInfos) { + // Normalize type name and compare segments against resource segments + const typeSegments = normalizeNameToSegments(typeInfo.typeName); + + // Check if any type name segment matches any resource segment + const typeNameMatches = typeSegments.some((typeSeg) => resourceSegments.toLowerCase() == typeSeg); + + // If type name segments match, create MatchDetail with typeId only + if (typeNameMatches) { + matches.push({ + typeId: typeInfo.typeId, + }); + } + + // For each field in type, check for matches + for (const field of typeInfo.fields) { + // Normalize field name and compare segments against resource segments + const fieldSegments = normalizeNameToSegments(field); + + // Check if any field segment matches any resource segment + const fieldMatches = fieldSegments.some((fieldSeg) => resourceSegments.toLowerCase() == fieldSeg); + console.log({ typeName: typeInfo.typeName, fieldSegments, fieldMatches, resourceSegments }); + + // If field segments match, create MatchDetail with typeId and fieldName + if (fieldMatches) { + matches.push({ + typeId: typeInfo.typeId, + fieldName: field, + }); + } + } + } + + // Collect ResourceMatch objects for resources with at least one match + if (matches.length > 0) { + resourceMatches.push({ + resource: resourceWrapper, + matches, + }); + } + } + + // Return array of ResourceMatch objects + return resourceMatches; +} + // TODO: change name to resource decider? // function eventDecider(service: Service) { -function eventDecider({ db, service }: { db: SpecDatabase; service: Service }) { - // TODO: need to get all the requred property field names here +function eventDecider({ + db, + service, + event, +}: { + db: SpecDatabase; + service: Service; + event: Event; +}): Link | undefined { + // Call extractEventTypeInfo to get type information + const typeInfos = extractEventTypeInfo(db, event); + + // Get resources using db.follow('hasResource', service) const resources = db.follow('hasResource', service); - if (service.name == 'aws-lambda') { - console.log({ resources: JSON.stringify(resources, null, 2) }); + console.log('Event Decider', { resources: JSON.stringify(resources, null, 2), eventName: event.name }); + + // Call matchTypesAndFieldsToResources to find matches + const resourceMatches = matchTypesAndFieldsToResources(Array.from(resources), typeInfos); + + // console.log('Event Decider', { serviceName: service.name }); + // Debug logging for aws-lambda service + if (service.name === 'aws-s3') { + console.log('=== AWS S3 Resource Matching Debug ==='); + console.log('Type Infos:', JSON.stringify(typeInfos, null, 2)); + console.log('Total Resources:', resources.length); + console.log('Matching Resources:', resourceMatches.length); + + // Log match details including type IDs and field names + if (resourceMatches.length > 0) { + console.log('Match Details:'); + resourceMatches.forEach((match, index) => { + console.log(` Resource ${index + 1}:`, match.resource.entity.cloudFormationType); + console.log(' Matches:'); + match.matches.forEach((detail) => { + if (detail.fieldName) { + console.log(` - Type ID: ${detail.typeId}, Field: ${detail.fieldName}`); + } else { + console.log(` - Type ID: ${detail.typeId}`); + } + }); + }); + } + + console.log('=========================================='); + } + + if (resourceMatches.length > 1) { + console.log('A single event matching multiple resources', { + resourceMatches: JSON.stringify(resourceMatches, null, 2), + }); + } + + // If matches found, return first matching resource + if (resourceMatches.length > 0) { + console.log( + `Event schema name: ${event.name}, matching resource name: ${resourceMatches[0].resource.entity.name} with cloudformation type: ${resourceMatches[0].resource.entity.cloudFormationType}`, + ); + return resourceMatches[0].resource; + } else if (resourceMatches.length == 0) { + console.log(`Event schema name: ${event.name}, doesn't match any resource in cloudformation`); } - // TODO: Change this to proper resource - return resources[0]; + // If no matches found, return undefined + return undefined; } export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) { @@ -158,16 +372,27 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) const service = allocateService({ eventSchemaName: event.SchemaName, db }); if (service == undefined) { + console.log(`The service related to this event schema name ${event.SchemaName} doesn't exist in CF`); // TODO: Maybe i need to return undefined return eventRet; } - const resource = allocateResource({ service, db }); + + // Move eventDB lookup before allocateResource call + const eventDB = db.lookup('event', 'name', 'equals', event.SchemaName).only(); + + // Pass eventDB as event parameter to allocateResource + const resource = allocateResource({ service, db, event: eventDB }); + // console.log('hasEvent link is creating...'); // console.log({ resource: JSON.stringify(resource), event: JSON.stringify(event) }); - const eventDB = db.lookup('event', 'name', 'equals', event.SchemaName).only(); - // TODO: should i return the entity only - db.link('hasEvent', resource.entity, eventDB); + // Check if resource is defined before calling db.link + // Only create hasEvent link when resource exists + if (resource) { + db.link('hasEvent', resource.entity, eventDB); + // TODO: add the identifier path + } + return eventRet; // FIX: i need to pass the specific detail object not like CF schema @@ -181,10 +406,10 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) for (const [name, property] of Object.entries(source.properties)) { try { - console.log('looping over the properties', { name, property }); + // console.log('looping over the properties', { name, property }); // FIX: this boolean should be something else let resolvedSchema = resolve(property, true); - console.log({ resolvedSchema }); + // console.log({ resolvedSchema }); // const relationships = collectPossibleRelationships(resolvedSchema); withResult(schemaTypeToModelType(name, resolvedSchema, fail.in(`property ${name}`)), (type) => { target.setProperty(name, { @@ -550,7 +775,7 @@ function removeUnionDuplicates(types: PropertyType[]) { throw new Error('Union cannot be empty'); } - for (let i = 0; i < types.length;) { + for (let i = 0; i < types.length; ) { const type = new RichPropertyType(types[i]); let dupe = false; diff --git a/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts b/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts index c026519eb..c7dcadd3d 100644 --- a/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts @@ -398,8 +398,8 @@ export namespace jsonschema { let current; if (weird == true) { current = root.Content; - console.log('WEIRD'); - console.log({ path, typeof: typeof root, root: JSON.stringify(root), current: JSON.stringify(current) }); + // console.log('WEIRD'); + // console.log({ path, typeof: typeof root, root: JSON.stringify(root), current: JSON.stringify(current) }); } else { current = root; // console.log('NOTWIERD'); @@ -415,8 +415,8 @@ export namespace jsonschema { } if (weird == true) { - console.log('WEIRD IN another place'); - console.log({ current: JSON.stringify(current) }); + // console.log('WEIRD IN another place'); + // console.log({ current: JSON.stringify(current) }); } if (current === undefined) { From 04f28bbc529c0ab7d962143f7cad23757febe458 Mon Sep 17 00:00:00 2001 From: Gasser Date: Thu, 13 Nov 2025 17:34:26 +0100 Subject: [PATCH 11/12] wip not working --- .../src/event-builder.ts | 17 ++- .../importers/import-eventbridge-schema.ts | 104 +++++++++++------- .../service-spec-types/src/types/event.ts | 6 +- 3 files changed, 81 insertions(+), 46 deletions(-) diff --git a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts index 6f8b84287..236c271ac 100644 --- a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts @@ -5,6 +5,7 @@ import { SpecDatabase, Event, EventTypeDefinition, + IdentifierPath, } from '@aws-cdk/service-spec-types'; import { AllFieldsGiven } from './diff-helpers'; import { jsonschema } from './types'; @@ -17,7 +18,7 @@ export interface EventBuilderOptions { } export class SpecBuilder { - constructor(public readonly db: SpecDatabase) {} + constructor(public readonly db: SpecDatabase) { } public eventBuilder(schemaName: string, options: EventBuilderOptions) { const existing = this.db.lookup('event', 'name', 'equals', schemaName); @@ -72,7 +73,7 @@ export class SpecBuilder { detailType: options.detailType, description: options.description, properties: {}, - identifiersPath: ['mockTypeName2.mockTypeName'], + identifiersPath: [], // attributes: {}, }); @@ -95,18 +96,24 @@ export class SpecBuilder { // interface ObjectWithProperties { properties: EventProperties; + identifiersPath: Array; } export class PropertyBagBuilder { protected candidateProperties: EventProperties = {}; + protected identifiersPath: Array = []; // @ts-ignore - constructor(private readonly _propertyBag: ObjectWithProperties) {} + constructor(private readonly _propertyBag: ObjectWithProperties) { } public setProperty(name: string, prop: EventProperty) { // console.log('Setting property', { prop, name }); this.candidateProperties[name] = prop; } + + public addIdentifierPath(identifierPath: IdentifierPath) { + this.identifiersPath.push(identifierPath); + } // // /** // * Delete a property from the builder @@ -451,7 +458,7 @@ export class EventTypeDefinitionBuilder extends PropertyBagBuilder { // private readonly fields: EventTypeDefinitionFields = {}; // @ts-ignore - constructor(public readonly db: SpecDatabase, private readonly typeDef: EventTypeDefinition) { + constructor(public readonly db: SpecDatabase, private readonly typeDef: ObjectWithProperties) { super(typeDef); } @@ -459,7 +466,7 @@ export class EventTypeDefinitionBuilder extends PropertyBagBuilder { // Object.assign(this.fields, fields); // } - public commit(): EventTypeDefinition { + public commit(): ObjectWithProperties { super.commit(); // Object.assign(this.typeDef, this.fields); return this.typeDef; diff --git a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts index 96535aeb8..2d27b8e4c 100644 --- a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts @@ -1,5 +1,26 @@ -import { PropertyType, RichPropertyType, Service, SpecDatabase, Event, Resource } from '@aws-cdk/service-spec-types'; -import { locateFailure, Fail, failure, isFailure, Result, tryCatch, using, ref, isSuccess, Link } from '@cdklabs/tskb'; +import { + PropertyType, + RichPropertyType, + Service, + SpecDatabase, + Event, + Resource, + EventTypeDefinition, + IdentifierPath, +} from '@aws-cdk/service-spec-types'; +import { + locateFailure, + Fail, + failure, + isFailure, + Result, + tryCatch, + using, + ref, + isSuccess, + Link, + Reference, +} from '@cdklabs/tskb'; import { SpecBuilder, PropertyBagBuilder } from '../event-builder'; import { ProblemReport, ReportAudience } from '../report'; import { unionSchemas } from '../schema-manipulation/unify-schemas'; @@ -50,7 +71,7 @@ function allocateResource({ db: SpecDatabase; service: Service; event: Event; -}): Link | undefined { +}): Resource | undefined { const resource = eventDecider({ service, db, event }); return resource; @@ -77,17 +98,14 @@ interface TypeInfo { /** * MatchDetail interface for tracking what matched */ -interface MatchDetail { - typeId: string; - fieldName?: string; -} /** * ResourceMatch interface for resources with matches */ interface ResourceMatch { + // TODO: remove Link type resource: Link; - matches: MatchDetail[]; + matches: IdentifierPath[]; } /** @@ -112,7 +130,7 @@ function normalizeNameToSegments(name: string): string[] { * Queries db.follow('eventUsesType', event) to get all type definitions * Returns array of TypeInfo objects with typeId, typeName, and fields */ -function extractEventTypeInfo(db: SpecDatabase, event: Event): TypeInfo[] { +function extractEventTypeInfo(db: SpecDatabase, event: Event): EventTypeDefinition[] { // Query for type definitions linked to the event const typeDefinitions = db.follow('eventUsesType', event); @@ -121,22 +139,24 @@ function extractEventTypeInfo(db: SpecDatabase, event: Event): TypeInfo[] { return []; } - // Extract type information from each type definition - const typeInfos: TypeInfo[] = []; - for (const typeDefWrapper of typeDefinitions) { - const typeDef = typeDefWrapper.entity; - const typeId = typeDef.$id; - const typeName = typeDef.name || ''; - const fields = typeDef.properties ? Object.keys(typeDef.properties) : []; - - typeInfos.push({ - typeId, - typeName, - fields, - }); - } + return typeDefinitions.map((x) => x.entity); - return typeInfos; + // Extract type information from each type definition + // const typeInfos: TypeInfo[] = []; + // for (const typeDefWrapper of typeDefinitions) { + // const typeDef = typeDefWrapper.entity; + // const typeId = typeDef.$id; + // const typeName = typeDef.name || ''; + // const fields = typeDef.properties ? Object.keys(typeDef.properties) : []; + // + // typeInfos.push({ + // typeId, + // typeName, + // fields, + // }); + // } + // + // return typeInfos; } /** @@ -144,13 +164,16 @@ function extractEventTypeInfo(db: SpecDatabase, event: Event): TypeInfo[] { * Compares normalized type names and field names against resource type names * Returns array of ResourceMatch objects for resources with at least one match */ -function matchTypesAndFieldsToResources(resources: Link[], typeInfos: TypeInfo[]): ResourceMatch[] { +function matchTypesAndFieldsToResources( + resources: Link[], + typeInfos: EventTypeDefinition[], +): ResourceMatch[] { const resourceMatches: ResourceMatch[] = []; // Iterate through all resources from service for (const resourceWrapper of resources) { const resource = resourceWrapper.entity; - const matches: MatchDetail[] = []; + const matches: IdentifierPath[] = []; // Extract resource type name from CloudFormation type // e.g., "Bucket" from "AWS::S3::Bucket" @@ -172,7 +195,7 @@ function matchTypesAndFieldsToResources(resources: Link[], typeInf // For each type info, compare against resource for (const typeInfo of typeInfos) { // Normalize type name and compare segments against resource segments - const typeSegments = normalizeNameToSegments(typeInfo.typeName); + const typeSegments = normalizeNameToSegments(typeInfo.name); // Check if any type name segment matches any resource segment const typeNameMatches = typeSegments.some((typeSeg) => resourceSegments.toLowerCase() == typeSeg); @@ -180,23 +203,23 @@ function matchTypesAndFieldsToResources(resources: Link[], typeInf // If type name segments match, create MatchDetail with typeId only if (typeNameMatches) { matches.push({ - typeId: typeInfo.typeId, + type: ref(typeInfo), }); } // For each field in type, check for matches - for (const field of typeInfo.fields) { + for (const field of Object.keys(typeInfo.properties)) { // Normalize field name and compare segments against resource segments const fieldSegments = normalizeNameToSegments(field); // Check if any field segment matches any resource segment const fieldMatches = fieldSegments.some((fieldSeg) => resourceSegments.toLowerCase() == fieldSeg); - console.log({ typeName: typeInfo.typeName, fieldSegments, fieldMatches, resourceSegments }); + console.log({ typeName: typeInfo.name, fieldSegments, fieldMatches, resourceSegments }); // If field segments match, create MatchDetail with typeId and fieldName if (fieldMatches) { matches.push({ - typeId: typeInfo.typeId, + type: ref(typeInfo), fieldName: field, }); } @@ -226,7 +249,7 @@ function eventDecider({ db: SpecDatabase; service: Service; event: Event; -}): Link | undefined { +}): Resource | undefined { // Call extractEventTypeInfo to get type information const typeInfos = extractEventTypeInfo(db, event); @@ -253,9 +276,9 @@ function eventDecider({ console.log(' Matches:'); match.matches.forEach((detail) => { if (detail.fieldName) { - console.log(` - Type ID: ${detail.typeId}, Field: ${detail.fieldName}`); + console.log(` - Type ID: ${detail.type}, Field: ${detail.fieldName}`); } else { - console.log(` - Type ID: ${detail.typeId}`); + console.log(` - Type ID: ${detail.type}`); } }); }); @@ -275,7 +298,7 @@ function eventDecider({ console.log( `Event schema name: ${event.name}, matching resource name: ${resourceMatches[0].resource.entity.name} with cloudformation type: ${resourceMatches[0].resource.entity.cloudFormationType}`, ); - return resourceMatches[0].resource; + return resourceMatches[0].resource.entity; } else if (resourceMatches.length == 0) { console.log(`Event schema name: ${event.name}, doesn't match any resource in cloudformation`); } @@ -389,11 +412,14 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) // Check if resource is defined before calling db.link // Only create hasEvent link when resource exists if (resource) { - db.link('hasEvent', resource.entity, eventDB); + // eventBuilder.addIdentifierPath() + db.link('hasEvent', resource, eventDB); // TODO: add the identifier path } + // FIX: I believe i need this line + return eventBuilder.commit(); - return eventRet; + // return eventRet; // FIX: i need to pass the specific detail object not like CF schema function recurseProperties(source: ImplicitJsonSchemaRecord, target: PropertyBagBuilder, fail: Fail) { @@ -647,7 +673,7 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) } } - return { type: 'ref', reference: ref(eventTypeDefinitionBuilder.commit()) }; + return { type: 'ref', reference: ref({ propertis: eventTypeDefinitionBuilder.commit().properties }) }; } function looksLikeBuiltinTagType(schema: jsonschema.Object): boolean { @@ -775,7 +801,7 @@ function removeUnionDuplicates(types: PropertyType[]) { throw new Error('Union cannot be empty'); } - for (let i = 0; i < types.length; ) { + for (let i = 0; i < types.length;) { const type = new RichPropertyType(types[i]); let dupe = false; diff --git a/packages/@aws-cdk/service-spec-types/src/types/event.ts b/packages/@aws-cdk/service-spec-types/src/types/event.ts index 9cebfc4d6..450105a43 100644 --- a/packages/@aws-cdk/service-spec-types/src/types/event.ts +++ b/packages/@aws-cdk/service-spec-types/src/types/event.ts @@ -1,4 +1,4 @@ -import { Entity, Relationship } from '@cdklabs/tskb'; +import { Entity, Reference, Relationship } from '@cdklabs/tskb'; import { PropertyType, Resource } from './resource'; export interface Event extends Entity { @@ -6,11 +6,13 @@ export interface Event extends Entity { readonly description: string; readonly source: string; readonly detailType: string; - readonly identifiersPath: Array; + readonly identifiersPath: Array; // TODO: i think i need some type related to typeDefinition readonly properties: EventProperties; } +export type IdentifierPath = { type: Reference; fieldName?: string }; + export type HasEvent = Relationship; // FIX: looks like having 2 properties aren't a good idea :D From 2024940cc06132815f820e6a68c225ddd460d66f Mon Sep 17 00:00:00 2001 From: Gasser Date: Thu, 13 Nov 2025 18:16:06 +0100 Subject: [PATCH 12/12] feat: add identifier path --- .../src/event-builder.ts | 15 ++++--- .../importers/import-eventbridge-schema.ts | 39 +++++++------------ 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts index 236c271ac..779efe444 100644 --- a/packages/@aws-cdk/service-spec-importers/src/event-builder.ts +++ b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts @@ -18,7 +18,7 @@ export interface EventBuilderOptions { } export class SpecBuilder { - constructor(public readonly db: SpecDatabase) { } + constructor(public readonly db: SpecDatabase) {} public eventBuilder(schemaName: string, options: EventBuilderOptions) { const existing = this.db.lookup('event', 'name', 'equals', schemaName); @@ -96,7 +96,7 @@ export class SpecBuilder { // interface ObjectWithProperties { properties: EventProperties; - identifiersPath: Array; + // identifiersPath: Array; } export class PropertyBagBuilder { @@ -104,7 +104,7 @@ export class PropertyBagBuilder { protected identifiersPath: Array = []; // @ts-ignore - constructor(private readonly _propertyBag: ObjectWithProperties) { } + constructor(private readonly _propertyBag: ObjectWithProperties) {} public setProperty(name: string, prop: EventProperty) { // console.log('Setting property', { prop, name }); @@ -133,6 +133,11 @@ export class PropertyBagBuilder { this.commitProperty(name, prop); } + // Commit identifier paths if the property bag is an Event + if ('identifiersPath' in this._propertyBag && this.identifiersPath.length > 0) { + (this._propertyBag as any).identifiersPath = this.identifiersPath; + } + return this._propertyBag; } @@ -458,7 +463,7 @@ export class EventTypeDefinitionBuilder extends PropertyBagBuilder { // private readonly fields: EventTypeDefinitionFields = {}; // @ts-ignore - constructor(public readonly db: SpecDatabase, private readonly typeDef: ObjectWithProperties) { + constructor(public readonly db: SpecDatabase, private readonly typeDef: EventTypeDefinition) { super(typeDef); } @@ -466,7 +471,7 @@ export class EventTypeDefinitionBuilder extends PropertyBagBuilder { // Object.assign(this.fields, fields); // } - public commit(): ObjectWithProperties { + public commit() { super.commit(); // Object.assign(this.typeDef, this.fields); return this.typeDef; diff --git a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts index 2d27b8e4c..41d479824 100644 --- a/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts +++ b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts @@ -8,19 +8,7 @@ import { EventTypeDefinition, IdentifierPath, } from '@aws-cdk/service-spec-types'; -import { - locateFailure, - Fail, - failure, - isFailure, - Result, - tryCatch, - using, - ref, - isSuccess, - Link, - Reference, -} from '@cdklabs/tskb'; +import { locateFailure, Fail, failure, isFailure, Result, tryCatch, using, ref, isSuccess, Link } from '@cdklabs/tskb'; import { SpecBuilder, PropertyBagBuilder } from '../event-builder'; import { ProblemReport, ReportAudience } from '../report'; import { unionSchemas } from '../schema-manipulation/unify-schemas'; @@ -71,7 +59,7 @@ function allocateResource({ db: SpecDatabase; service: Service; event: Event; -}): Resource | undefined { +}): { resource: Resource; matchDetail: IdentifierPath[] } | undefined { const resource = eventDecider({ service, db, event }); return resource; @@ -89,11 +77,11 @@ function allocateResource({ /** * TypeInfo interface for event type definitions */ -interface TypeInfo { - typeId: string; - typeName: string; - fields: string[]; -} +// interface TypeInfo { +// typeId: string; +// typeName: string; +// fields: string[]; +// } /** * MatchDetail interface for tracking what matched @@ -249,7 +237,7 @@ function eventDecider({ db: SpecDatabase; service: Service; event: Event; -}): Resource | undefined { +}): { resource: Resource; matchDetail: IdentifierPath[] } | undefined { // Call extractEventTypeInfo to get type information const typeInfos = extractEventTypeInfo(db, event); @@ -298,7 +286,7 @@ function eventDecider({ console.log( `Event schema name: ${event.name}, matching resource name: ${resourceMatches[0].resource.entity.name} with cloudformation type: ${resourceMatches[0].resource.entity.cloudFormationType}`, ); - return resourceMatches[0].resource.entity; + return { resource: resourceMatches[0].resource.entity, matchDetail: resourceMatches[0].matches }; } else if (resourceMatches.length == 0) { console.log(`Event schema name: ${event.name}, doesn't match any resource in cloudformation`); } @@ -412,8 +400,9 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) // Check if resource is defined before calling db.link // Only create hasEvent link when resource exists if (resource) { - // eventBuilder.addIdentifierPath() - db.link('hasEvent', resource, eventDB); + console.log({ event: event.SchemaName, matches: resource.matchDetail[0] }); + eventBuilder.addIdentifierPath(resource.matchDetail[0]); + db.link('hasEvent', resource.resource, eventDB); // TODO: add the identifier path } // FIX: I believe i need this line @@ -673,7 +662,7 @@ export function importEventBridgeSchema(options: LoadEventBridgeSchmemaOptions) } } - return { type: 'ref', reference: ref({ propertis: eventTypeDefinitionBuilder.commit().properties }) }; + return { type: 'ref', reference: ref(eventTypeDefinitionBuilder.commit()) }; } function looksLikeBuiltinTagType(schema: jsonschema.Object): boolean { @@ -801,7 +790,7 @@ function removeUnionDuplicates(types: PropertyType[]) { throw new Error('Union cannot be empty'); } - for (let i = 0; i < types.length;) { + for (let i = 0; i < types.length; ) { const type = new RichPropertyType(types[i]); let dupe = false;