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/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..6559dad1a --- /dev/null +++ b/packages/@aws-cdk/service-spec-importers/schemas/EventBridge.schema.json @@ -0,0 +1,95 @@ +{ + "$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" + } + }, + "required": [ + "SchemaName", + "Content", + "Description" + ], + "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..1a4cc97c5 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,33 @@ 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, + }); + // const existing = db.lookup('event', 'name', 'equals', event.SchemaName.split('@')[1]); + // console.log('db-builder', { existing }); + } + } + }); + } + /** * Look at a load result and report problems */ 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 new file mode 100644 index 000000000..779efe444 --- /dev/null +++ b/packages/@aws-cdk/service-spec-importers/src/event-builder.ts @@ -0,0 +1,491 @@ +import { + EventProperties, + EventProperty, + RichProperty, + SpecDatabase, + Event, + EventTypeDefinition, + IdentifierPath, +} from '@aws-cdk/service-spec-types'; +import { AllFieldsGiven } from './diff-helpers'; +import { jsonschema } from './types'; + +// TODO: those aren't optional put them somehow required +export interface EventBuilderOptions { + readonly source: string; + readonly detailType: string; + readonly description: string; +} + +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('eventTypeDefinition', { + // 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('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, + source: options.source, + detailType: options.detailType, + description: options.description, + properties: {}, + identifiersPath: [], + // attributes: {}, + }); + + // this.db.link('eventUsesType', event, typeDef); + // this.db.link('eventUsesType', event, typeDef2); + // mocking type ends + // + // TODO: add more information for the 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); + } +} +// +interface ObjectWithProperties { + properties: EventProperties; + // identifiersPath: Array; +} + +export class PropertyBagBuilder { + protected candidateProperties: EventProperties = {}; + protected identifiersPath: Array = []; + + // @ts-ignore + 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 + // * + // * 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); + } + + // 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; + } + + private commitProperty(name: string, prop: EventProperty) { + 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: 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, + 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: EventProperty) { + if (!prop.required) { + delete prop.required; + } + // if (prop.causesReplacement === 'no') { + // delete prop.causesReplacement; + // } + } +} + +export class EventBuilder extends PropertyBagBuilder { + private eventTypeDefinitions = 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.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; + // } + // + // // 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.event.additionalReplacementProperties) { + // this.event.additionalReplacementProperties = []; + // } + // this.event.additionalReplacementProperties.push(propPath); + // } + // } + // } + // + // public markDeprecatedProperties(...props: string[]) { + // for (const propName of props) { + // (this.candidateProperties[propName] ?? {}).deprecated = Deprecation.WARN; + // } + // } + // + // public setTagInformation(tagInfo: TagInformation) { + // this.event.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.event.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.event.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.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 eventTypeDefinitionBuilder( + typeName: string, + options?: { description?: string; schema?: jsonschema.RecordLikeObject }, + ) { + 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; + // } + 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 { + eventTypeDefinitionBuilder: new EventTypeDefinitionBuilder(this.db, existing), + freshInDb: true, + freshInSession: true, + }; + } + return { + eventTypeDefinitionBuilder: new EventTypeDefinitionBuilder(this.db, existing), + freshInDb: false, + freshInSession, + }; + } + + const typeDef = this.db.allocate('eventTypeDefinition', { + name: typeName, + properties: {}, + }); + this.db.link('eventUsesType', this.event, typeDef); + this.eventTypeDefinitions.set(typeName, typeDef); + + const builder = new EventTypeDefinitionBuilder(this.db, typeDef); + return { eventTypeDefinitionBuilder: 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 type EventTypeDefinitionFields = Pick; + +export class EventTypeDefinitionBuilder extends PropertyBagBuilder { + // private readonly fields: EventTypeDefinitionFields = {}; + + // @ts-ignore + constructor(public readonly db: SpecDatabase, private readonly typeDef: EventTypeDefinition) { + super(typeDef); + } + + // public setFields(fields: EventTypeDefinitionFields) { + // Object.assign(this.fields, fields); + // } + + public commit() { + super.commit(); + // Object.assign(this.typeDef, this.fields); + return this.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 new file mode 100644 index 000000000..41d479824 --- /dev/null +++ b/packages/@aws-cdk/service-spec-importers/src/importers/import-eventbridge-schema.ts @@ -0,0 +1,818 @@ +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 } from '@cdklabs/tskb'; +import { SpecBuilder, PropertyBagBuilder } from '../event-builder'; +import { ProblemReport, ReportAudience } from '../report'; +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; +} + +// 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, + event, +}: { + db: SpecDatabase; + service: Service; + event: Event; +}): { resource: Resource; matchDetail: IdentifierPath[] } | 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? + // const resource = this.db.allocate('service', { + // name, + // shortName, + // capitalized, + // cloudFormationNamespace, + // }); + + // return resource; +} + +/** + * TypeInfo interface for event type definitions + */ +// interface TypeInfo { +// typeId: string; +// typeName: string; +// fields: string[]; +// } + +/** + * MatchDetail interface for tracking what matched + */ + +/** + * ResourceMatch interface for resources with matches + */ +interface ResourceMatch { + // TODO: remove Link type + resource: Link; + matches: IdentifierPath[]; +} + +/** + * 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): EventTypeDefinition[] { + // 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 []; + } + + return typeDefinitions.map((x) => x.entity); + + // 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: EventTypeDefinition[], +): ResourceMatch[] { + const resourceMatches: ResourceMatch[] = []; + + // Iterate through all resources from service + for (const resourceWrapper of resources) { + const resource = resourceWrapper.entity; + const matches: IdentifierPath[] = []; + + // 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.name); + + // 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({ + type: ref(typeInfo), + }); + } + + // For each field in type, check for matches + 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.name, fieldSegments, fieldMatches, resourceSegments }); + + // If field segments match, create MatchDetail with typeId and fieldName + if (fieldMatches) { + matches.push({ + type: ref(typeInfo), + 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, + event, +}: { + db: SpecDatabase; + service: Service; + event: Event; +}): { resource: Resource; matchDetail: IdentifierPath[] } | 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); + 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.type}, Field: ${detail.fieldName}`); + } else { + console.log(` - Type ID: ${detail.type}`); + } + }); + }); + } + + 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 { 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`); + } + + // If no matches found, return undefined + return undefined; +} + +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'], + description: event.Description, + }); + + if (eventBuilder == undefined) { + return; + } + // @ts-ignore + const eventFailure = failure.in(event.SchemaName); + + 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]; + } + + // Get the type name from the reference (e.g., "ScheduledEvent") + const detailTypeName = lastKey; + + // Determine if detail is required + // @ts-ignore + 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)); + + const eventRet = eventBuilder.commit(); + + 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; + } + + // 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) }); + + // Check if resource is defined before calling db.link + // Only create hasEvent link when resource exists + if (resource) { + 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 + return 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) { + 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}`), + ); + } + } + } + + /** + * 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 { 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) { + // eventTypeDefinitionBuilder.setFields({ documentation: schema.description }); + // } + if (jsonschema.isRecordLikeObject(schema)) { + recurseProperties(schema, eventTypeDefinitionBuilder, fail.in(`typedef ${nameHint}`)); + } + } + + return { type: 'ref', reference: ref(eventTypeDefinitionBuilder.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 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); + // } + // } +} + +// 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; + 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..ebfa026df --- /dev/null +++ b/packages/@aws-cdk/service-spec-importers/src/types/eventbridge/EventBridgeSchema.ts @@ -0,0 +1,21 @@ +import { ImplicitJsonSchemaRecord } from '../registry-schema/CloudFormationRegistrySchema'; + +export interface EventBridgeSchema extends ImplicitJsonSchemaRecord { + readonly SchemaName: string; + readonly Description: 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-importers/src/types/registry-schema/JsonSchema.ts b/packages/@aws-cdk/service-spec-importers/src/types/registry-schema/JsonSchema.ts index bd2e639bc..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 @@ -359,7 +359,7 @@ 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 => { // Don't resolve again if schema is already resolved if (isResolvedSchema(ref)) { return ref; @@ -395,7 +395,16 @@ export namespace jsonschema { } const parts = path.substring(2).split('/'); - let current = root; + 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) }); + } else { + current = root; + // console.log('NOTWIERD'); + // console.log({ typeof: typeof root, root: JSON.stringify(root), current: JSON.stringify(root) }); + } let lastKey: string | undefined; while (true) { if (parts.length === 0) { @@ -404,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/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..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,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, EventTypeDefinition } from './event'; import { DimensionSet, Metric, @@ -36,6 +37,7 @@ export function emptyDatabase() { name: fieldIndex('name', stringCmp), cloudFormationNamespace: fieldIndex('cloudFormationNamespace', stringCmp), }), + eventTypeDefinition: entityCollection(), typeDefinition: entityCollection(), augmentations: entityCollection(), metric: entityCollection().index({ @@ -46,6 +48,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 +63,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', '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 new file mode 100644 index 000000000..450105a43 --- /dev/null +++ b/packages/@aws-cdk/service-spec-types/src/types/event.ts @@ -0,0 +1,36 @@ +import { Entity, Reference, Relationship } from '@cdklabs/tskb'; +import { PropertyType, Resource } from './resource'; + +export interface Event extends Entity { + readonly name: string; + readonly description: 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 IdentifierPath = { type: Reference; fieldName?: string }; + +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; + //FIX: 99% this need to be deleted + required?: boolean; +} + +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 {