Skip to content

Commit b0e4d53

Browse files
authored
fix(sam): Reduce SAM schema size (#283)
1 parent e99f68d commit b0e4d53

File tree

2 files changed

+143
-6
lines changed

2 files changed

+143
-6
lines changed

src/schema/SamSchemaTransformer.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ export const SamSchemaTransformer = {
5050
string,
5151
unknown
5252
>;
53+
let propertiesRefKey: string | undefined;
5354
if (propertiesSchema?.$ref) {
54-
const refKey = (propertiesSchema.$ref as string).replace('#/definitions/', '');
55-
propertiesSchema = samSchema.definitions[refKey] as Record<string, unknown>;
55+
propertiesRefKey = (propertiesSchema.$ref as string).replace('#/definitions/', '');
56+
propertiesSchema = samSchema.definitions[propertiesRefKey] as Record<string, unknown>;
5657
}
5758

5859
const cfnSchema: CloudFormationResourceSchema = {
@@ -63,7 +64,7 @@ export const SamSchemaTransformer = {
6364
(propertiesSchema?.properties as Record<string, unknown>) ?? {},
6465
samSchema.definitions,
6566
),
66-
definitions: samSchema.definitions,
67+
definitions: this.extractReferencedDefinitions(definition, samSchema.definitions, defKey),
6768
additionalProperties: false,
6869
required: (propertiesSchema?.required as string[]) ?? [],
6970
attributes: {}, // Empty to avoid GetAtt issues
@@ -79,6 +80,47 @@ export const SamSchemaTransformer = {
7980
return resourceSchemas;
8081
},
8182

83+
extractReferencedDefinitions(
84+
schema: Record<string, unknown> | undefined,
85+
allDefinitions: Record<string, unknown>,
86+
rootDefinitionKey?: string,
87+
): Record<string, unknown> {
88+
const referenced = new Set<string>();
89+
const result: Record<string, unknown> = {};
90+
91+
// Include the root definition if provided
92+
if (rootDefinitionKey && allDefinitions[rootDefinitionKey]) {
93+
referenced.add(rootDefinitionKey);
94+
result[rootDefinitionKey] = allDefinitions[rootDefinitionKey];
95+
}
96+
97+
const collectRefs = (obj: unknown): void => {
98+
if (typeof obj === 'object' && obj !== null) {
99+
if (Array.isArray(obj)) {
100+
for (const item of obj) {
101+
collectRefs(item);
102+
}
103+
} else {
104+
const record = obj as Record<string, unknown>;
105+
if (record.$ref && typeof record.$ref === 'string') {
106+
const refKey = record.$ref.replace('#/definitions/', '');
107+
if (!referenced.has(refKey) && allDefinitions[refKey]) {
108+
referenced.add(refKey);
109+
result[refKey] = allDefinitions[refKey];
110+
collectRefs(allDefinitions[refKey]);
111+
}
112+
}
113+
for (const value of Object.values(record)) {
114+
collectRefs(value);
115+
}
116+
}
117+
}
118+
};
119+
120+
collectRefs(schema);
121+
return result;
122+
},
123+
82124
resolvePropertyTypes(
83125
properties: Record<string, unknown>,
84126
definitions: Record<string, unknown>,
@@ -98,6 +140,18 @@ export const SamSchemaTransformer = {
98140
property = { ...property, description: property.markdownDescription };
99141
}
100142

143+
// Convert SAM's non-standard additionalProperties usage to CloudFormation-compliant patternProperties
144+
if (property.additionalProperties && typeof property.additionalProperties === 'object') {
145+
const additionalProps = property.additionalProperties as Record<string, unknown>;
146+
property = {
147+
...property,
148+
additionalProperties: false,
149+
patternProperties: {
150+
'.*': additionalProps,
151+
},
152+
};
153+
}
154+
101155
// If property already has a type, keep it
102156
if (property.type) {
103157
return property;

tst/unit/schema/SamSchemaTransformer.test.ts

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,95 @@ describe('SamSchemaTransformer', () => {
7474
const result = SamSchemaTransformer.transformSamSchema(mockSamSchema);
7575
const functionSchema = result.get('AWS::Serverless::Function')!;
7676

77-
// Verify all definitions are included
77+
// Verify only referenced definitions are included (full reference chain)
7878
expect(functionSchema.definitions).toBeDefined();
79-
expect(functionSchema.definitions).toEqual(mockSamSchema.definitions);
79+
80+
const expectedDefinitions = {
81+
aws_serverless_functionResource: {
82+
properties: {
83+
Type: { enum: ['AWS::Serverless::Function'] },
84+
Properties: { $ref: '#/definitions/FunctionProperties' },
85+
},
86+
},
87+
FunctionProperties: {
88+
properties: {
89+
Runtime: { type: 'string' },
90+
CodeUri: { $ref: '#/definitions/CodeUriType' },
91+
},
92+
},
93+
CodeUriType: {
94+
type: 'string',
95+
},
96+
};
97+
98+
expect(functionSchema.definitions).toEqual(expectedDefinitions);
8099

81100
// This ensures $refs like { $ref: '#/definitions/CodeUriType' } will work
82101
expect(functionSchema.definitions!.CodeUriType).toEqual({ type: 'string' });
83-
expect(functionSchema.definitions!.SomeOtherDefinition).toEqual({ type: 'object' });
102+
// SomeOtherDefinition should not be included as it's not referenced
103+
expect(functionSchema.definitions!.SomeOtherDefinition).toBeUndefined();
104+
});
105+
106+
test('should handle deep nested references and convert additionalProperties', () => {
107+
const mockSamSchema = {
108+
properties: {
109+
Resources: {
110+
additionalProperties: {
111+
anyOf: [{ $ref: '#/definitions/aws_serverless_functionResource' }],
112+
},
113+
},
114+
},
115+
definitions: {
116+
aws_serverless_functionResource: {
117+
properties: {
118+
Type: { enum: ['AWS::Serverless::Function'] },
119+
Properties: { $ref: '#/definitions/FunctionProperties' },
120+
},
121+
},
122+
FunctionProperties: {
123+
properties: {
124+
Events: {
125+
additionalProperties: { $ref: '#/definitions/EventDefinition' },
126+
type: 'object',
127+
},
128+
},
129+
},
130+
EventDefinition: {
131+
properties: {
132+
Properties: { $ref: '#/definitions/EventProperties' },
133+
},
134+
},
135+
EventProperties: {
136+
properties: {
137+
Auth: { $ref: '#/definitions/ApiAuth' },
138+
},
139+
},
140+
ApiAuth: {
141+
properties: {
142+
ApiKeyRequired: { type: 'boolean' },
143+
},
144+
},
145+
UnusedDefinition: { type: 'string' },
146+
},
147+
};
148+
149+
const result = SamSchemaTransformer.transformSamSchema(mockSamSchema);
150+
const functionSchema = result.get('AWS::Serverless::Function')!;
151+
152+
// Should include all definitions in the nested chain
153+
expect(functionSchema.definitions!.aws_serverless_functionResource).toBeDefined();
154+
expect(functionSchema.definitions!.FunctionProperties).toBeDefined();
155+
expect(functionSchema.definitions!.EventDefinition).toBeDefined();
156+
expect(functionSchema.definitions!.EventProperties).toBeDefined();
157+
expect(functionSchema.definitions!.ApiAuth).toBeDefined();
158+
159+
// Should exclude unused definition
160+
expect(functionSchema.definitions!.UnusedDefinition).toBeUndefined();
161+
162+
// Should convert additionalProperties to patternProperties
163+
const eventsProperty = functionSchema.properties.Events as any;
164+
expect(eventsProperty.additionalProperties).toBe(false);
165+
expect(eventsProperty.patternProperties).toBeDefined();
166+
expect(eventsProperty.patternProperties['.*']).toEqual({ $ref: '#/definitions/EventDefinition' });
84167
});
85168
});

0 commit comments

Comments
 (0)