fix(core): Pick output schemas by parameter value for Gmail and Notion (no-changelog) (#40757)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Mutasem Aldmour 2026-10-14 09:11:49 +00:00 committed by GitHub
parent 066480d8db
commit 0b4c99f10d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 4574 additions and 873 deletions

View File

@ -481,6 +481,7 @@ describe('generateSimulationFixtures', () => {
resource: 'message',
operation: 'post',
hasOutputParser: false,
parameters: { resource: 'message', operation: 'post' },
});
const promptText = (generate.mock.calls[0][0] as Array<{ content: Array<{ text: string }> }>)[0]
.content[0].text;

View File

@ -4470,6 +4470,179 @@ describe('generate-types', () => {
expect(content).toContain('output?: Items<FreshserviceV1TicketGetOutput>');
});
describe('parameter variants', () => {
const combo = { resource: 'ticket', operation: 'get' };
const plainSchema = { type: 'object', properties: { From: { type: 'string' } } };
const rawVariant = {
resource: 'ticket',
operation: 'get',
variant: 'simple-false',
schema: { type: 'object', properties: { text: { type: 'string' } } },
};
const nodeWithSimple: NodeTypeDescription = {
...mockFreshserviceNode,
properties: [
...mockFreshserviceNode.properties,
{
displayName: 'Simplify',
name: 'simple',
type: 'boolean',
default: true,
displayOptions: { show: { resource: ['ticket'], operation: ['get'] } },
},
],
};
const generate = (variants: Array<typeof rawVariant>, node = nodeWithSimple) =>
generateTypes.generateDiscriminatorFile(
node,
1,
combo,
generateTypes.getPropertiesForCombination(node, combo),
plainSchema,
6,
variants,
);
it('emits an output type for each variant and keeps the plain one for the default', () => {
const content = generate([rawVariant]);
expect(content).toContain('export type FreshserviceV1TicketGetOutput = {');
expect(content).toContain('From?: string;');
expect(content).toContain('export type FreshserviceV1TicketGetSimpleFalseOutput = {');
expect(content).toContain('text?: string;');
});
it('emits one node type for each parameter value, narrowing the parameter', () => {
const content = generate([rawVariant]);
expect(content).toContain('export type FreshserviceV1TicketGetSimpleTrueNode = {');
expect(content).toContain(
'config: NodeConfig<FreshserviceV1TicketGetParams & { simple?: true }>',
);
expect(content).toContain('output?: Items<FreshserviceV1TicketGetOutput>;');
expect(content).toContain('export type FreshserviceV1TicketGetSimpleFalseNode = {');
expect(content).toContain(
'config: NodeConfig<FreshserviceV1TicketGetParams & { simple: false }>',
);
expect(content).toContain('output?: Items<FreshserviceV1TicketGetSimpleFalseOutput>;');
});
it('returns either output when the parameter is an expression', () => {
const content = generate([rawVariant]);
expect(content).toContain('export type FreshserviceV1TicketGetSimpleExpressionNode = {');
expect(content).toContain(
'config: NodeConfig<FreshserviceV1TicketGetParams & { simple: Expression<boolean> }>',
);
expect(content).toContain(
'output?: Items<FreshserviceV1TicketGetOutput | FreshserviceV1TicketGetSimpleFalseOutput>;',
);
});
it('exports the node type as a union of the parameter variants', () => {
const content = generate([rawVariant]);
expect(content).toMatch(
/export type FreshserviceV1TicketGetNode =\s+\| FreshserviceV1TicketGetSimpleTrueNode\s+\| FreshserviceV1TicketGetSimpleFalseNode\s+\| FreshserviceV1TicketGetSimpleExpressionNode;/,
);
});
it('ignores a variant whose parameter the node does not have', () => {
const content = generate(
[{ ...rawVariant, variant: 'unknown-false' }],
mockFreshserviceNode,
);
expect(content).toContain('export type FreshserviceV1TicketGetNode = {');
expect(content).not.toContain('SimpleFalse');
});
describe('options parameters', () => {
const nodeWithOptions = (
values: Array<number | string | boolean>,
): NodeTypeDescription => ({
...mockFreshserviceNode,
properties: [
...mockFreshserviceNode.properties,
{
displayName: 'Format',
name: 'format',
type: 'options',
default: values[0],
options: values.map((value) => ({ name: String(value), value })),
displayOptions: { show: { resource: ['ticket'], operation: ['get'] } },
},
],
});
const modeVariant = (value: string) => ({
...rawVariant,
variant: `format-${value}`,
});
it('keeps the numeric type of numeric option values', () => {
const content = generate([modeVariant('1')], nodeWithOptions([0, 1]));
expect(content).toContain('{ format?: 0 }');
expect(content).toContain('{ format: 1 }');
expect(content).toContain('{ format: Expression<number> }');
expect(content).not.toContain("'1'");
});
it('keeps string option values as string literals', () => {
const content = generate([modeVariant('slow')], nodeWithOptions(['fast', 'slow']));
expect(content).toContain("{ format?: 'fast' }");
expect(content).toContain("{ format: 'slow' }");
expect(content).toContain('{ format: Expression<string> }');
});
it('ignores a variant whose value is not a number for numeric options', () => {
const content = generate([modeVariant('slow')], nodeWithOptions([0, 1]));
expect(content).toContain('export type FreshserviceV1TicketGetNode = {');
expect(content).not.toContain('FormatSlow');
});
it('ignores a variant whose value would not be a safe type literal', () => {
const content = generate([modeVariant("x'y")], nodeWithOptions(['fast', "x'y"]));
expect(content).toContain('export type FreshserviceV1TicketGetNode = {');
});
it('gives negative and positive numeric values different type names', () => {
const content = generate(
[modeVariant('-1'), modeVariant('1')],
nodeWithOptions([0, -1, 1]),
);
expect(content).toContain('{ format: -1 }');
expect(content).toContain('{ format: 1 }');
expect(content.match(/export type FreshserviceV1TicketGetFormat1Node =/g)).toHaveLength(
1,
);
expect(content).toContain('export type FreshserviceV1TicketGetFormatMinus1Node =');
});
it('ignores the variants when two values would get the same type name', () => {
const content = generate(
[modeVariant('a-b'), modeVariant('a_b')],
nodeWithOptions(['fast', 'a-b', 'a_b']),
);
expect(content).toContain('export type FreshserviceV1TicketGetNode = {');
});
it('types the variants of a boolean-valued options parameter', () => {
const content = generate([modeVariant('false')], nodeWithOptions([true, false]));
expect(content).toContain('{ format?: true }');
expect(content).toContain('{ format: false }');
expect(content).toContain('{ format: Expression<boolean> }');
});
});
});
it('should inline credentials interface when node has credentials', () => {
const combo = { resource: 'ticket', operation: 'get' };
const props = generateTypes.getPropertiesForCombination(mockFreshserviceNode, combo);
@ -5625,6 +5798,61 @@ describe('generate-types', () => {
});
});
describe('jsonSchemaToTypeScript with additionalProperties', () => {
it('adds an index signature when an object has properties and an additionalProperties schema', () => {
const result = generateTypes.jsonSchemaToTypeScript({
type: 'object',
properties: { id: { type: 'string' } },
additionalProperties: { type: ['number', 'null'] },
});
expect(result).toContain('id?: string;');
expect(result).toContain('[key: string]: number | null | string | undefined;');
});
it('keeps the declared property types in the index signature union once', () => {
const result = generateTypes.jsonSchemaToTypeScript({
type: 'object',
properties: { id: { type: 'string' }, name: { type: 'string' } },
additionalProperties: { type: 'string' },
});
expect(result).toContain('[key: string]: string | undefined;');
});
it('keeps nested object types intact in the index signature union', () => {
const result = generateTypes.jsonSchemaToTypeScript({
type: 'object',
properties: { date: { type: 'object', properties: { end: { type: ['string', 'null'] } } } },
additionalProperties: { type: ['string', 'null'] },
});
expect(result).toContain('end?: string | null;');
expect(result).toMatch(
/\[key: string\]: string \| null \| \{[\s\S]*end\?: string \| null;[\s\S]*\} \| undefined;/,
);
});
it('does not add an index signature for additionalProperties: true', () => {
const result = generateTypes.jsonSchemaToTypeScript({
type: 'object',
properties: { id: { type: 'string' } },
additionalProperties: true,
});
expect(result).not.toContain('[key: string]');
});
it('still emits a Record when an object has only additionalProperties', () => {
const result = generateTypes.jsonSchemaToTypeScript({
type: 'object',
additionalProperties: { type: 'number' },
});
expect(result).toBe('Record<string, number>');
});
});
describe('discoverSchemasForNode', () => {
// Use real filesystem with temp directories under NODES_BASE_DIST
const NODES_BASE_DIST = path.resolve(__dirname, '../../../../nodes-base/dist/nodes');
@ -5678,6 +5906,39 @@ describe('generate-types', () => {
}
});
it('discovers <operation>.<param>-<value>.json files as variants, not as operations', () => {
const nodeName = '__TestParameterVariants__';
const plain = { type: 'object', properties: { From: { type: 'string' } } };
const raw = { type: 'object', properties: { text: { type: 'string' } } };
try {
createTestSchemaDir(nodeName, 'v1.0.0', {
'message/get.json': JSON.stringify(plain),
'message/get.simple-false.json': JSON.stringify(raw),
});
const result = generateTypes.discoverSchemasForNode(
`n8n-nodes-base.${nodeName}`,
1,
nodeName,
);
expect(result).toHaveLength(2);
expect(result).toContainEqual({ resource: 'message', operation: 'get', schema: plain });
expect(result).toContainEqual({
resource: 'message',
operation: 'get',
variant: 'simple-false',
schema: raw,
});
expect(generateTypes.findSchemaForOperation(result, 'message', 'get')?.schema).toEqual(
plain,
);
} finally {
cleanupTestDir(nodeName);
}
});
it('exact-matches fractional typeVersions against minor-versioned dirs', () => {
const nodeName = '__TestMinorVersionExact__';
const schema = { type: 'object', properties: { id: { type: 'string' } } };

View File

@ -411,6 +411,11 @@ export interface VersionGroup {
export interface OutputSchema {
resource: string;
operation: string;
/**
* `<param>-<value>` for a `<operation>.<param>-<value>.json` file: the shape
* returned when the node parameter has that value. Absent for the plain file.
*/
variant?: string;
schema: JsonSchema;
}
@ -843,9 +848,11 @@ function collectSchemasFromVersionDir(
for (const opEntry of operations) {
if (!opEntry.isFile() || !opEntry.name.endsWith('.json')) continue;
const operationName = opEntry.name.replace('.json', '');
if (seen.has(`${entry.name}/${operationName}`)) continue;
const fileKey = opEntry.name.replace('.json', '');
if (seen.has(`${entry.name}/${fileKey}`)) continue;
const schemaPath = path.join(resourceDir, opEntry.name);
// `<operation>.<variant>.json` is a parameter-dependent shape of the operation.
const [operationName, variant] = splitOperationVariant(fileKey);
try {
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
@ -853,9 +860,10 @@ function collectSchemasFromVersionDir(
schemas.push({
resource: entry.name,
operation: operationName,
...(variant ? { variant } : {}),
schema,
});
seen.add(`${entry.name}/${operationName}`);
seen.add(`${entry.name}/${fileKey}`);
} catch {
// Skip invalid JSON files
}
@ -980,6 +988,19 @@ function orderedVersionDirectories(schemaDir: string, version: number): string[]
}
}
/**
* The type of an index signature that sits next to declared properties. It must
* accept the declared property types too, and `undefined` for optional ones.
*/
function indexSignatureType(
additionalType: string,
declaredTypes: string[],
hasOptional: boolean,
): string {
const members = [additionalType, ...declaredTypes, ...(hasOptional ? ['undefined'] : [])];
return [...new Set(members)].join(' | ');
}
/**
* Convert a JSON Schema to TypeScript type string
*
@ -1049,12 +1070,21 @@ export function jsonSchemaToTypeScript(schema: JsonSchema, indent = 0): string {
case 'object':
if (schema.properties && Object.keys(schema.properties).length > 0) {
const required = new Set(schema.required ?? []);
const declaredTypes: string[] = [];
const props = Object.entries(schema.properties).map(([key, propSchema]) => {
const optional = required.has(key) ? '' : '?';
const quotedKey = needsQuoting(key) ? `'${key}'` : key;
const propType = jsonSchemaToTypeScript(propSchema, indent + 1);
declaredTypes.push(propType);
return `${nextIndent}${quotedKey}${optional}: ${propType};`;
});
if (typeof schema.additionalProperties === 'object') {
const additionalType = jsonSchemaToTypeScript(schema.additionalProperties, indent + 1);
const hasOptional = Object.keys(schema.properties).some((key) => !required.has(key));
props.push(
`${nextIndent}[key: string]: ${indexSignatureType(additionalType, declaredTypes, hasOptional)};`,
);
}
return `{\n${props.join('\n')}\n${indentStr}}`;
}
if (schema.additionalProperties) {
@ -1083,7 +1113,31 @@ export function findSchemaForOperation(
operation: string,
): OutputSchema | undefined {
return schemas.find(
(s) => s.resource.toLowerCase() === resource.toLowerCase() && s.operation === operation,
(s) =>
!s.variant &&
s.resource.toLowerCase() === resource.toLowerCase() &&
s.operation === operation,
);
}
/** `get.simple-false` → `['get', 'simple-false']`; a name without a dot has no variant. */
function splitOperationVariant(fileKey: string): [string, string | undefined] {
const dot = fileKey.indexOf('.');
if (dot <= 0) return [fileKey, undefined];
return [fileKey.slice(0, dot), fileKey.slice(dot + 1)];
}
/** The parameter-dependent shapes of one operation (`<operation>.<param>-<value>.json`). */
export function findVariantSchemasForOperation(
schemas: OutputSchema[],
resource: string,
operation: string,
): OutputSchema[] {
return schemas.filter(
(s) =>
s.variant !== undefined &&
s.resource.toLowerCase() === resource.toLowerCase() &&
s.operation === operation,
);
}
@ -2992,6 +3046,156 @@ export function generateSharedFile(
return lines.join('\n');
}
/**
* The parameter that selects between several output shapes of one operation,
* with the shape for each of its values.
*/
type ParameterValueType = 'boolean' | 'number' | 'string';
interface ParameterVariants {
param: string;
/** Primitive type of the parameter values: it decides the literal and expression types. */
valueType: ParameterValueType;
/** The value used when the parameter is absent. The plain schema describes it. */
defaultValue: string;
others: Array<{ value: string; schema: JsonSchema }>;
}
/** `simple-false` → parameter `simple`, value `false`. */
function parseParameterVariant(variant: string): { param: string; value: string } | undefined {
const separator = variant.indexOf('-');
if (separator <= 0) return undefined;
return { param: variant.slice(0, separator), value: variant.slice(separator + 1) };
}
/**
* Turn the variant schemas of an operation into one `ParameterVariants`, or
* undefined when they cannot be typed: no plain schema, several parameters,
* a parameter the node does not have, or no default to attach the plain schema to.
*/
function resolveParameterVariants(
props: NodeProperty[],
variantSchemas: OutputSchema[],
): ParameterVariants | undefined {
const parsed = variantSchemas.map((s) => ({
...parseParameterVariant(s.variant ?? ''),
schema: s.schema,
}));
const param = parsed[0]?.param;
if (!param || parsed.some((entry) => entry.param !== param || entry.value === undefined)) {
return undefined;
}
const property = props.find((p) => p.name === param);
const valueType = property ? getParameterValueType(property) : undefined;
if (!property || !valueType) return undefined;
const defaultValue = String(property.default);
const others = parsed.map((entry) => ({ value: entry.value!, schema: entry.schema }));
if (
others.some((entry) => entry.value === defaultValue || !isTypeSafeValue(valueType, entry.value))
) {
return undefined;
}
const suffixes = [defaultValue, ...others.map((entry) => entry.value)].map((value) =>
parameterValueSuffix(valueType, value),
);
if (new Set(suffixes).size !== suffixes.length) return undefined;
return {
param,
valueType,
defaultValue,
others: others.sort((a, b) => a.value.localeCompare(b.value)),
};
}
/** The value type of a boolean parameter, or of an options parameter whose default is a string, number or boolean. */
function getParameterValueType(property: NodeProperty): ParameterValueType | undefined {
if (property.type === 'boolean') {
return typeof property.default === 'boolean' ? 'boolean' : undefined;
}
if (property.type !== 'options') return undefined;
if (typeof property.default === 'number') return 'number';
if (typeof property.default === 'boolean') return 'boolean';
return typeof property.default === 'string' ? 'string' : undefined;
}
/**
* The part of a type name that stands for a parameter value. Signs and decimal
* points get words, because `toPascalCase` drops them and `-1` and `1` would
* share a name.
*/
function parameterValueSuffix(valueType: ParameterValueType, value: string): string {
if (valueType === 'number') return value.replace('-', 'Minus').replace('.', 'Point');
return toPascalCase(value);
}
/** A value comes from a file name, so it must be a valid literal of its type and safe to emit. */
function isTypeSafeValue(valueType: ParameterValueType, value: string): boolean {
if (valueType === 'boolean') return value === 'true' || value === 'false';
if (valueType === 'number')
return Number.isFinite(Number(value)) && String(Number(value)) === value;
return /^[\w-]+$/.test(value);
}
/** `true` / `'raw'` — the TypeScript literal for a parameter value. */
function parameterValueLiteral(variants: ParameterVariants, value: string): string {
return variants.valueType === 'string' ? `'${value}'` : value;
}
/**
* Emit the output types and node types of an operation whose output shape
* depends on one parameter: a node type for each value, plus one for an
* expression (which can be either), and a union of them under `nodeTypeName`.
*/
function generateParameterVariantTypes(
variants: ParameterVariants,
names: { base: string; output: string; node: string },
nodeBody: (typeName: string, paramsConstraint: string, outputType: string) => string[],
): string[] {
const { param, defaultValue, others } = variants;
const suffixOf = (value: string) =>
`${toPascalCase(param)}${parameterValueSuffix(variants.valueType, value)}`;
const outputOf = (value: string) => `${names.base}${suffixOf(value)}Output`;
const nodeOf = (value: string) => `${names.base}${suffixOf(value)}Node`;
const lines: string[] = [];
for (const { value, schema: variantSchema } of others) {
lines.push(`export type ${outputOf(value)} = ${jsonSchemaToTypeScript(variantSchema)};`, '');
}
const members = [
{
node: nodeOf(defaultValue),
constraint: `${param}?: ${parameterValueLiteral(variants, defaultValue)}`,
output: names.output,
},
...others.map(({ value }) => ({
node: nodeOf(value),
constraint: `${param}: ${parameterValueLiteral(variants, value)}`,
output: outputOf(value),
})),
];
const allOutputs = members.map((member) => member.output).join(' | ');
members.push({
node: `${names.base}${toPascalCase(param)}ExpressionNode`,
constraint: `${param}: Expression<${variants.valueType}>`,
output: allOutputs,
});
for (const member of members) {
lines.push(...nodeBody(member.node, `{ ${member.constraint} }`, member.output), '');
}
lines.push(`export type ${names.node} =`);
members.forEach((member, index) => {
lines.push(`${INDENT}| ${member.node}${index === members.length - 1 ? ';' : ''}`);
});
return lines;
}
/**
* Generate a discriminator file for a single resource/operation (or mode, etc.) combination
*
@ -3009,6 +3213,7 @@ export function generateDiscriminatorFile(
props: NodeProperty[],
schema?: JsonSchema,
_importDepth: number = 5,
variantSchemas: OutputSchema[] = [],
): string {
const prefix = getPackagePrefix(node.name);
const nodeName = prefix + toPascalCase(getNodeBaseName(node.name));
@ -3156,26 +3361,44 @@ export function generateDiscriminatorFile(
}
}
lines.push(`export type ${nodeTypeName} = {`);
lines.push(`${INDENT}type: '${node.name}';`);
lines.push(`${INDENT}version: ${version};`);
if (isTrigger) {
lines.push(`${INDENT}isTrigger: true;`);
}
// Include subnodes in config if AI inputs exist
// subnodes field is required if any AI input type is required
const hasRequiredSubnodes = aiInputTypes.some((input) => input.required);
const configType = buildNodeConfigType(configName, {
credentialsTypeName:
node.credentials && node.credentials.length > 0 ? 'Credentials' : undefined,
subnodeConfigTypeName: subnodeConfigTypeName ?? undefined,
subnodesRequired: hasRequiredSubnodes,
});
lines.push(`${INDENT}config: ${configType};`);
if (schema) {
lines.push(`${INDENT}output?: Items<${outputTypeName}>;`);
const nodeBody = (typeName: string, paramsConstraint?: string, outputType?: string) => {
const paramsType = paramsConstraint ? `${configName} & ${paramsConstraint}` : configName;
const configType = buildNodeConfigType(paramsType, {
credentialsTypeName:
node.credentials && node.credentials.length > 0 ? 'Credentials' : undefined,
subnodeConfigTypeName: subnodeConfigTypeName ?? undefined,
subnodesRequired: hasRequiredSubnodes,
});
return [
`export type ${typeName} = {`,
`${INDENT}type: '${node.name}';`,
`${INDENT}version: ${version};`,
...(isTrigger ? [`${INDENT}isTrigger: true;`] : []),
`${INDENT}config: ${configType};`,
...(outputType ? [`${INDENT}output?: Items<${outputType}>;`] : []),
'};',
];
};
const variants = schema ? resolveParameterVariants(props, variantSchemas) : undefined;
if (variants) {
lines.push(
...generateParameterVariantTypes(
variants,
{
base: `${nodeName}${versionSuffix}${comboSuffix}`,
output: outputTypeName,
node: nodeTypeName,
},
nodeBody,
),
);
} else {
lines.push(...nodeBody(nodeTypeName, undefined, schema ? outputTypeName : undefined));
}
lines.push('};');
return lines.join('\n');
}
@ -3372,6 +3595,7 @@ export function planSplitVersionFiles(
props,
matchingSchema?.schema,
6,
findVariantSchemasForOperation(outputSchemas, resource, operation),
);
files.set(filePath, content);
}

View File

@ -45,6 +45,7 @@ export function buildSchemaContexts(
resource,
operation,
hasOutputParser: node.name ? outputParserTargets?.has(node.name) === true : false,
parameters: params,
});
const nodeName = node.name ?? node.type;

View File

@ -88,6 +88,7 @@ describe('buildSchemaContexts', () => {
resource: 'row',
operation: 'get',
hasOutputParser: false,
parameters: workflow.nodes[0].parameters,
});
expect(contexts[0].schema).toEqual({ type: 'object', properties: { id: {} } });
});

View File

@ -23,6 +23,8 @@ export type OutputSchemaLookup = (node: {
operation?: string;
/** Node has an `ai_outputParser` attached — resolves the `with-parser` layout variant. */
hasOutputParser?: boolean;
/** The node's parameters — pick a shape variant such as `simple: false`. */
parameters?: Record<string, unknown>;
}) => Record<string, unknown> | undefined;
/** Structured-output-parser info for an AI root node (Agent/Chain). */

View File

@ -0,0 +1,13 @@
import { GMAIL_GUIDE } from './gmail';
describe('GMAIL_GUIDE', () => {
it('names the real Simplify parameter, `simple`, and not `simplify`', () => {
expect(GMAIL_GUIDE.content).toContain('**simple: false**');
expect(GMAIL_GUIDE.content).toContain('**simple: true**');
expect(GMAIL_GUIDE.content).not.toMatch(/\bsimplify\b\s*(:|to\b)/i);
});
it('does not give the message output shape as the shape of a thread', () => {
expect(GMAIL_GUIDE.content).toContain('thread:get');
});
});

View File

@ -14,15 +14,21 @@ export const GMAIL_GUIDE: NodeTypeGuide = {
- **authentication**: OAuth2 or Service Account
#### Simplify Option (CRITICAL)
- **simplify: false** - Returns FULL email with body, attachments, headers
- **simplify: true** - Returns LIMITED data (subject, snippet only, NO body)
The parameter is named **simple**, not "simplify". Default: true.
- **simple: false** - Returns the FULL parsed email with body, attachments and headers. The sender is the object \`$json.from\`: read the address with \`$json.from.value[0].address\`. The body is in \`$json.text\` and \`$json.html\`.
- **simple: true** - Returns a LIMITED flat item (subject, snippet, labels, NO body). The sender is the string \`$json.From\`, for example "Sara Chen <sara@example.com>".
ALWAYS set simplify to FALSE when:
- Workflow needs to analyze email content/body
- AI Agent will process or summarize emails
- Email content is used for decision making
Read the sender from the field that matches the setting. \`$json.From\` is undefined when simple is false.
These shapes are for the **message** operations. For **thread:get**, both settings flatten the headers, so \`$json.From\` works. With simple false, thread:get returns the Gmail API payload: the body is encoded in base64url in \`payload.parts[].body.data\` (look through nested parts), and there is no \`$json.from\`, \`$json.text\` or \`$json.html\`.
Set simple to false only when:
- Workflow needs the email body to analyze or summarize it
- AI Agent will process the email text
- Attachments need to be accessed
Keep simple true when the workflow only filters or routes by sender, subject or labels.
#### Gmail Filter Queries
For Gmail Trigger nodes, use comprehensive search filters with OR operators:
- Multiple keywords: "subject:(delivery OR shipment OR package OR tracking)"
@ -40,7 +46,7 @@ NOT just: "subject:delivery" (misses many relevant emails)
- Configure to, subject, and message fields
2. **Reading Emails for AI Analysis**:
- Set simplify to false (required for full email body)
- Set simple to false (required for full email body) and read the sender from \`$json.from.value[0].address\`
- Configure appropriate filter query
- Pass full email data to AI Agent

View File

@ -10,6 +10,7 @@ import { CUSTOM_NODES_PACKAGE_NAME, CustomDirectoryLoader, DirectoryLoader } fro
import type {
ICredentialType,
INodeProperties,
INodeType,
INodeTypeDescription,
KnownNodesAndCredentials,
NodeLoader,
@ -290,6 +291,80 @@ describe('LoadNodesAndCredentials', () => {
).toEqual({ type: 'object' });
});
describe('parameter variants', () => {
const rawSchema = { type: 'object', title: 'raw' };
const plainSchema = { type: 'object' };
const lookupRef = {
type: 'n8n-nodes-base.test',
typeVersion: 1,
resource: 'account',
operation: 'get',
};
const provideNode = (nodeType: Partial<INodeType>) => {
const type = { description: { properties: [] }, ...nodeType } as INodeType;
vi.spyOn(instance, 'getNode').mockReturnValue({ sourcePath: 'Test.node.js', type });
};
beforeEach(() => {
const accountDir = join(nodesDir, 'Test', '__schema__', 'v1.0.0', 'account');
writeFileSync(join(accountDir, 'get.simple-false.json'), JSON.stringify(rawSchema));
writeFileSync(
join(accountDir, 'get.custom.json'),
JSON.stringify({ type: 'object', title: 'custom' }),
);
});
it('should pick the variant file that matches the node parameters', () => {
provideNode({});
expect(
instance.createOutputSchemaLookup()({ ...lookupRef, parameters: { simple: false } }),
).toEqual(rawSchema);
});
it('should use the property default when the parameter is absent', () => {
provideNode({
description: {
properties: [
{
name: 'simple',
type: 'boolean',
default: false,
displayOptions: { show: { resource: ['account'], operation: ['get'] } },
},
],
} as unknown as INodeTypeDescription,
});
expect(instance.createOutputSchemaLookup()({ ...lookupRef, parameters: {} })).toEqual(
rawSchema,
);
});
it('should use the plain file when the parameter keeps its default', () => {
provideNode({
description: {
properties: [{ name: 'simple', type: 'boolean', default: true }],
} as unknown as INodeTypeDescription,
});
expect(instance.createOutputSchemaLookup()({ ...lookupRef, parameters: {} })).toEqual(
plainSchema,
);
});
it('should still resolve the plain schema when the node cannot be loaded', () => {
vi.spyOn(instance, 'getNode').mockImplementation(() => {
throw new Error('not loadable');
});
expect(
instance.createOutputSchemaLookup()({ ...lookupRef, parameters: { simple: true } }),
).toEqual(plainSchema);
});
});
it('should return undefined for unknown nodes or missing schemas', () => {
const lookup = instance.createOutputSchemaLookup();
expect(

View File

@ -0,0 +1,64 @@
import type { INodeProperties } from 'n8n-workflow';
import { getParameterDefaults } from '../output-schema-parameter-defaults';
const property = (overrides: Partial<INodeProperties> & { name: string }): INodeProperties => ({
displayName: overrides.name,
type: 'boolean',
default: true,
...overrides,
});
describe('getParameterDefaults', () => {
it('returns the default of each property', () => {
const defaults = getParameterDefaults(
[property({ name: 'simple', default: true }), property({ name: 'limit', default: 50 })],
{},
);
expect(defaults).toEqual({ simple: true, limit: 50 });
});
it('returns nothing when the node has no properties', () => {
expect(getParameterDefaults(undefined, {})).toEqual({});
});
it('skips a property that is not shown for the resource and operation', () => {
const defaults = getParameterDefaults(
[
property({
name: 'simple',
default: false,
displayOptions: { show: { resource: ['draft'], operation: ['get'] } },
}),
property({
name: 'simple',
default: true,
displayOptions: { show: { resource: ['message'], operation: ['get'] } },
}),
],
{ resource: 'message', operation: 'get' },
);
expect(defaults).toEqual({ simple: true });
});
it('keeps the first default when a name repeats', () => {
const defaults = getParameterDefaults(
[property({ name: 'simple', default: true }), property({ name: 'simple', default: false })],
{},
);
expect(defaults).toEqual({ simple: true });
});
it.each(['constructor', 'toString', '__proto__'])(
'keeps the default of a property named %s',
(name) => {
const defaults = getParameterDefaults([property({ name, default: false })], {});
expect(Object.hasOwn(defaults, name)).toBe(true);
expect(Object.getOwnPropertyDescriptor(defaults, name)?.value).toBe(false);
},
);
});

View File

@ -40,11 +40,17 @@ import type {
NodeLoader,
} from 'n8n-workflow';
import { ensureError } from '@n8n/utils/errors/ensure-error';
import { injectDomainRestrictionFields, UnexpectedError, UserError } from 'n8n-workflow';
import {
injectDomainRestrictionFields,
NodeHelpers,
UnexpectedError,
UserError,
} from 'n8n-workflow';
import path from 'path';
import picocolors from 'picocolors';
import { CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_NAME, CLI_DIR, inE2ETests } from '@/constants';
import { getParameterDefaults } from '@/output-schema-parameter-defaults';
import { createAiTools, createHitlTools } from '@/tool-generation';
@Service()
@ -310,10 +316,12 @@ export class LoadNodesAndCredentials {
* community nodes and production installs alike.
*/
createOutputSchemaLookup(): OutputSchemaLookup {
return ({ type, typeVersion, resource, operation, hasOutputParser }) => {
return ({ type, typeVersion, resource, operation, hasOutputParser, parameters }) => {
const nodePath = this.known.nodes[type]?.sourcePath;
if (!nodePath) return undefined;
const nodeType = this.tryGetVersionedNodeType(type, typeVersion);
return loadOutputSchema({
nodeDir: path.dirname(nodePath),
version: typeVersion,
@ -321,10 +329,24 @@ export class LoadNodesAndCredentials {
operation,
versionFallback: true,
variant: hasOutputParser ? OUTPUT_PARSER_SCHEMA_VARIANT : undefined,
parameters,
parameterDefaults: getParameterDefaults(nodeType?.description.properties, {
resource,
operation,
}),
});
};
}
/** Schema previews are best-effort, so a node that cannot load just has no variants. */
private tryGetVersionedNodeType(type: string, version: number): INodeType | undefined {
try {
return NodeHelpers.getVersionedNodeType(this.getNode(type).type, version);
} catch {
return undefined;
}
}
getCustomDirectories(): string[] {
const customDirectories = [this.instanceSettings.customExtensionDir];

View File

@ -0,0 +1,38 @@
import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow';
interface SchemaContext {
resource?: string;
operation?: string;
}
/** A property whose `displayOptions.show` lists other values than ours is not shown. */
function isShownFor(property: INodeProperties, context: SchemaContext): boolean {
const show = property.displayOptions?.show;
if (!show) return true;
return (['resource', 'operation'] as const).every((key) => {
const shownValues = show[key];
const value = context[key];
return !Array.isArray(shownValues) || value === undefined || shownValues.includes(value);
});
}
/**
* Defaults of the parameters that apply to one resource/operation, so a
* parameter missing from a saved node can still pick its schema variant.
* The first matching property wins when a name repeats per operation.
*/
export function getParameterDefaults(
properties: INodeProperties[] | undefined,
context: SchemaContext,
): Record<string, NodeParameterValueType> {
// A Map keeps names such as `constructor` and `__proto__` as plain keys.
const defaults = new Map<string, NodeParameterValueType>();
for (const property of properties ?? []) {
if (defaults.has(property.name) || !isShownFor(property, context)) continue;
defaults.set(property.name, property.default);
}
return Object.fromEntries(defaults);
}

View File

@ -202,6 +202,105 @@ describe('output-schema-resolver', () => {
).toBe(plain);
});
describe('parameter variants', () => {
const ref = { version: 2, resource: 'message', operation: 'get', versionFallback: true };
it('picks the <operation>.<param>-<value>.json file that matches the parameters', () => {
writeSchema('v2.0.0/message/get.json');
const raw = writeSchema('v2.0.0/message/get.simple-false.json');
expect(resolveOutputSchemaPath({ nodeDir, ...ref, parameters: { simple: false } })).toBe(
raw,
);
});
it('uses the plain file when the parameters do not match a variant', () => {
const plain = writeSchema('v2.0.0/message/get.json');
writeSchema('v2.0.0/message/get.simple-false.json');
expect(resolveOutputSchemaPath({ nodeDir, ...ref, parameters: { simple: true } })).toBe(
plain,
);
expect(resolveOutputSchemaPath({ nodeDir, ...ref })).toBe(plain);
});
it('applies the parameter default when the parameter is absent', () => {
const plain = writeSchema('v2.0.0/message/get.json');
const raw = writeSchema('v2.0.0/message/get.simple-false.json');
expect(
resolveOutputSchemaPath({
nodeDir,
...ref,
parameters: {},
parameterDefaults: { simple: true },
}),
).toBe(plain);
expect(
resolveOutputSchemaPath({
nodeDir,
...ref,
parameters: {},
parameterDefaults: { simple: false },
}),
).toBe(raw);
});
it('does not apply the default when the parameter is set to null', () => {
const plain = writeSchema('v2.0.0/message/get.json');
writeSchema('v2.0.0/message/get.simple-false.json');
expect(
resolveOutputSchemaPath({
nodeDir,
...ref,
parameters: { simple: null },
parameterDefaults: { simple: false },
}),
).toBe(plain);
});
it('applies the default when the parameter is set to undefined', () => {
writeSchema('v2.0.0/message/get.json');
const raw = writeSchema('v2.0.0/message/get.simple-false.json');
expect(
resolveOutputSchemaPath({
nodeDir,
...ref,
parameters: { simple: undefined },
parameterDefaults: { simple: false },
}),
).toBe(raw);
});
it('ignores a parameter that only exists on the object prototype', () => {
const plain = writeSchema('v2.0.0/message/get.json');
writeSchema('v2.0.0/message/get.constructor-true.json');
expect(resolveOutputSchemaPath({ nodeDir, ...ref, parameters: {} })).toBe(plain);
});
it('ignores variant files whose name has characters outside word characters and dashes', () => {
const plain = writeSchema('v2.0.0/message/get.json');
writeSchema('v2.0.0/message/get.a b-true.json');
writeSchema('v2.0.0/message/get.a.b-true.json');
expect(
resolveOutputSchemaPath({ nodeDir, ...ref, parameters: { 'a b': true, 'a.b': true } }),
).toBe(plain);
});
it('checks each version directory for the variant before using an older version', () => {
writeSchema('v1.0.0/message/get.simple-false.json');
const plain = writeSchema('v2.0.0/message/get.json');
expect(resolveOutputSchemaPath({ nodeDir, ...ref, parameters: { simple: false } })).toBe(
plain,
);
});
});
it('returns undefined when __schema__ does not exist', () => {
expect(
resolveOutputSchemaPath({

View File

@ -28,6 +28,14 @@ export interface OutputSchemaRef {
* resource/operation, and only with `versionFallback`.
*/
variant?: string;
/**
* Node parameters that pick a shape variant. A file `<operation>.<param>-<value>.json`
* is used when the parameter (or, if absent, its default) equals `<value>`.
* Only meaningful for refs with an operation, and only with `versionFallback`.
*/
parameters?: Record<string, unknown>;
/** Defaults for parameters missing from `parameters`, read from the node description. */
parameterDefaults?: Record<string, unknown>;
}
/**
@ -47,6 +55,8 @@ export type OutputSchemaLookup = (node: {
operation?: string;
/** Node has an `ai_outputParser` attached — resolves the `with-parser` layout variant. */
hasOutputParser?: boolean;
/** The node's parameters — pick a shape variant such as `simple: false`. */
parameters?: Record<string, unknown>;
}) => Record<string, unknown> | undefined;
/** Pad "1" / "1.2" to the on-disk "1.0.0" / "1.2.0" directory format. */
@ -94,6 +104,64 @@ function orderFallbackCandidates(dirNames: string[], target: number[]): string[]
].map((d) => d.name);
}
/** Variant names are file-name fragments: word characters and dashes only. */
const VARIANT_NAME_PATTERN = /^[\w-]+$/;
/** `simple-false` → parameter `simple`, value `false`. */
function parseParameterVariant(variant: string): { param: string; value: string } | undefined {
const separator = variant.indexOf('-');
if (separator <= 0) return undefined;
return { param: variant.slice(0, separator), value: variant.slice(separator + 1) };
}
/**
* The value a node parameter has: its own value, even when null, else its default.
* Own-property checks keep names such as `constructor` from reading the prototype.
*/
function effectiveParameterValue(ref: OutputSchemaRef, param: string): unknown {
const { parameters, parameterDefaults } = ref;
if (parameters && Object.hasOwn(parameters, param) && parameters[param] !== undefined) {
return parameters[param];
}
return parameterDefaults && Object.hasOwn(parameterDefaults, param)
? parameterDefaults[param]
: undefined;
}
function variantMatchesParameters(variant: string, ref: OutputSchemaRef): boolean {
const parsed = parseParameterVariant(variant);
if (!parsed) return false;
const effective = effectiveParameterValue(ref, parsed.param);
const isPrimitive = ['string', 'number', 'boolean'].includes(typeof effective);
return isPrimitive && String(effective) === parsed.value;
}
/** Variant names that have a `<operation>.<variant>.json` file in `dir`, sorted. */
function listOperationVariants(dir: string, operation: string): string[] {
try {
return readdirSync(dir)
.filter((file) => file.startsWith(`${operation}.`) && file.endsWith('.json'))
.map((file) => file.slice(operation.length + 1, -'.json'.length))
.filter((variant) => VARIANT_NAME_PATTERN.test(variant))
.sort();
} catch {
return [];
}
}
/**
* The `<operation>.<param>-<value>.json` file for this ref in one version dir:
* the first file whose name matches the node parameters.
*/
function findOperationVariantFile(ref: OutputSchemaRef, versionPath: string): string | undefined {
const { operation, resource } = ref;
if (!operation) return undefined;
const dir = path.resolve(versionPath, resource ?? '');
const variants = listOperationVariants(dir, operation);
const match = variants.find((name) => variantMatchesParameters(name, ref));
return match ? path.join(dir, `${operation}.${match}.json`) : undefined;
}
/**
* Resolve an output schema reference to a `.json` file path, or undefined
* when the resolved path would escape the node's `__schema__` directory.
@ -136,6 +204,8 @@ export function resolveOutputSchemaPath(ref: OutputSchemaRef): string | undefine
const hasDiscriminators = Boolean(resource) || Boolean(operation);
for (const versionDir of [...new Set([exactVersionDir, ...available])]) {
const variantFile = findOperationVariantFile(ref, path.join(schemaBaseDir, versionDir));
if (variantFile && isContainedWithin(schemaBaseDir, variantFile)) return variantFile;
const filePath = buildPath(versionDir);
if (filePath && existsSync(filePath)) return filePath;
if (!hasDiscriminators) {

View File

@ -17,7 +17,7 @@ import {
prepareQuery,
simplifyOutput,
} from './GenericFunctions';
import { simplifyMemoryNotice } from './utils/descriptions';
import { simplifyMemoryNotice, simplifyOutputShapeHint } from './utils/descriptions';
import type {
GmailTriggerFilters,
GmailTriggerOptions,
@ -155,8 +155,7 @@ When this trigger feeds an action that creates records (tasks, rows, tickets, me
description:
'Whether to return a simplified version of the response instead of the raw data',
builderHint: {
propertyHint:
'Keep true by default. When true, returns lightweight metadata (id, threadId, labels, subject, from, to, snippet). When false, fetches and parses the full raw email (adds html, text, textAsHtml, headers, attachments), which uses much more memory and is a common cause of out-of-memory crashes. Only set false when the email body is actually required.',
propertyHint: simplifyOutputShapeHint,
},
},
simplifyMemoryNotice({ displayOptions: { show: { simple: [false] } } }),

View File

@ -1,145 +1,61 @@
{
"type": "object",
"properties": {
"date": {
"type": "string"
},
"from": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"headers": {
"type": "object",
"properties": {
"arc-authentication-results": {
"type": "string"
},
"arc-message-signature": {
"type": "string"
},
"arc-seal": {
"type": "string"
},
"authentication-results": {
"type": "string"
},
"content-type": {
"type": "string"
},
"date": {
"type": "string"
},
"delivered-to": {
"type": "string"
},
"dkim-signature": {
"type": "string"
},
"from": {
"type": "string"
},
"message-id": {
"type": "string"
},
"mime-version": {
"type": "string"
},
"received": {
"type": "string"
},
"received-spf": {
"type": "string"
},
"return-path": {
"type": "string"
},
"subject": {
"type": "string"
},
"to": {
"type": "string"
},
"x-google-smtp-source": {
"type": "string"
},
"x-received": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
}
},
"messageId": {
"type": "string"
},
"sizeEstimate": {
"type": "integer"
},
"subject": {
"type": "string"
},
"text": {
"type": "string"
},
"textAsHtml": {
"type": "string"
},
"threadId": {
"type": "string"
},
"to": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
}
},
"version": 6
"type": "object",
"properties": {
"Bcc": {
"type": "string"
},
"Cc": {
"type": "string"
},
"From": {
"type": "string"
},
"Subject": {
"type": "string"
},
"To": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"mimeType": {
"type": "string"
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"threadId": {
"type": "string"
}
},
"version": 1
}

View File

@ -0,0 +1,235 @@
{
"type": "object",
"properties": {
"bcc": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"cc": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"date": {
"type": "string"
},
"from": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"headers": {
"type": "object",
"properties": {
"arc-authentication-results": {
"type": "string"
},
"arc-message-signature": {
"type": "string"
},
"arc-seal": {
"type": "string"
},
"authentication-results": {
"type": "string"
},
"content-type": {
"type": "string"
},
"date": {
"type": "string"
},
"delivered-to": {
"type": "string"
},
"dkim-signature": {
"type": "string"
},
"from": {
"type": "string"
},
"message-id": {
"type": "string"
},
"mime-version": {
"type": "string"
},
"received": {
"type": "string"
},
"received-spf": {
"type": "string"
},
"return-path": {
"type": "string"
},
"subject": {
"type": "string"
},
"to": {
"type": "string"
},
"x-google-smtp-source": {
"type": "string"
},
"x-received": {
"type": "string"
}
},
"additionalProperties": {
"type": "string"
}
},
"html": {
"type": ["string", "boolean"]
},
"id": {
"type": "string"
},
"inReplyTo": {
"type": "string"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
}
},
"messageId": {
"type": "string"
},
"references": {
"type": "array",
"items": {
"type": "string"
}
},
"replyTo": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"sizeEstimate": {
"type": "integer"
},
"subject": {
"type": "string"
},
"text": {
"type": "string"
},
"textAsHtml": {
"type": "string"
},
"threadId": {
"type": "string"
},
"to": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
}
},
"version": 6
}

View File

@ -1,145 +1,61 @@
{
"type": "object",
"properties": {
"date": {
"type": "string"
},
"from": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"headers": {
"type": "object",
"properties": {
"arc-authentication-results": {
"type": "string"
},
"arc-message-signature": {
"type": "string"
},
"arc-seal": {
"type": "string"
},
"authentication-results": {
"type": "string"
},
"content-type": {
"type": "string"
},
"date": {
"type": "string"
},
"delivered-to": {
"type": "string"
},
"dkim-signature": {
"type": "string"
},
"from": {
"type": "string"
},
"message-id": {
"type": "string"
},
"mime-version": {
"type": "string"
},
"received": {
"type": "string"
},
"received-spf": {
"type": "string"
},
"return-path": {
"type": "string"
},
"subject": {
"type": "string"
},
"to": {
"type": "string"
},
"x-gm-message-state": {
"type": "string"
},
"x-google-dkim-signature": {
"type": "string"
},
"x-google-smtp-source": {
"type": "string"
},
"x-received": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
}
},
"messageId": {
"type": "string"
},
"sizeEstimate": {
"type": "integer"
},
"subject": {
"type": "string"
},
"threadId": {
"type": "string"
},
"to": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
}
},
"version": 7
"type": "object",
"properties": {
"Bcc": {
"type": "string"
},
"Cc": {
"type": "string"
},
"From": {
"type": "string"
},
"Subject": {
"type": "string"
},
"To": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"mimeType": {
"type": "string"
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"threadId": {
"type": "string"
}
},
"version": 1
}

View File

@ -0,0 +1,235 @@
{
"type": "object",
"properties": {
"bcc": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"cc": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"date": {
"type": "string"
},
"from": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"headers": {
"type": "object",
"properties": {
"arc-authentication-results": {
"type": "string"
},
"arc-message-signature": {
"type": "string"
},
"arc-seal": {
"type": "string"
},
"authentication-results": {
"type": "string"
},
"content-type": {
"type": "string"
},
"date": {
"type": "string"
},
"delivered-to": {
"type": "string"
},
"dkim-signature": {
"type": "string"
},
"from": {
"type": "string"
},
"message-id": {
"type": "string"
},
"mime-version": {
"type": "string"
},
"received": {
"type": "string"
},
"received-spf": {
"type": "string"
},
"return-path": {
"type": "string"
},
"subject": {
"type": "string"
},
"to": {
"type": "string"
},
"x-google-smtp-source": {
"type": "string"
},
"x-received": {
"type": "string"
}
},
"additionalProperties": {
"type": "string"
}
},
"html": {
"type": ["string", "boolean"]
},
"id": {
"type": "string"
},
"inReplyTo": {
"type": "string"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
}
},
"messageId": {
"type": "string"
},
"references": {
"type": "array",
"items": {
"type": "string"
}
},
"replyTo": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"sizeEstimate": {
"type": "integer"
},
"subject": {
"type": "string"
},
"text": {
"type": "string"
},
"textAsHtml": {
"type": "string"
},
"threadId": {
"type": "string"
},
"to": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
}
},
"version": 6
}

View File

@ -1,173 +1,124 @@
{
"type": "object",
"properties": {
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"ARC-Authentication-Results": {
"type": "string"
},
"ARC-Message-Signature": {
"type": "string"
},
"ARC-Seal": {
"type": "string"
},
"Authentication-Results": {
"type": "string"
},
"Content-Type": {
"type": "string"
},
"Date": {
"type": "string"
},
"Delivered-To": {
"type": "string"
},
"DKIM-Signature": {
"type": "string"
},
"From": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"Message-ID": {
"type": "string"
},
"MIME-Version": {
"type": "string"
},
"payload": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
}
}
}
}
}
},
"Received": {
"type": "string"
},
"Received-SPF": {
"type": "string"
},
"Return-Path": {
"type": "string"
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"Subject": {
"type": "string"
},
"threadId": {
"type": "string"
},
"To": {
"type": "string"
},
"X-Google-Smtp-Source": {
"type": "string"
},
"X-Received": {
"type": "string"
}
}
}
}
},
"version": 4
"type": "object",
"properties": {
"Bcc": {
"type": "string"
},
"Cc": {
"type": "string"
},
"From": {
"type": "string"
},
"Subject": {
"type": "string"
},
"To": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"mimeType": {
"type": "string"
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"threadId": {
"type": "string"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Bcc": {
"type": "string"
},
"Cc": {
"type": "string"
},
"From": {
"type": "string"
},
"Subject": {
"type": "string"
},
"To": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"mimeType": {
"type": "string"
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"threadId": {
"type": "string"
}
}
}
}
},
"version": 1
}

View File

@ -0,0 +1,440 @@
{
"type": "object",
"properties": {
"Cc": {
"type": "string"
},
"Date": {
"type": "string"
},
"From": {
"type": "string"
},
"Subject": {
"type": "string"
},
"To": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"threadId": {
"type": "string"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Cc": {
"type": "string"
},
"Date": {
"type": "string"
},
"From": {
"type": "string"
},
"Subject": {
"type": "string"
},
"To": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"threadId": {
"type": "string"
}
},
"additionalProperties": {
"type": "string"
}
}
}
},
"version": 1,
"additionalProperties": {
"type": "string"
}
}

View File

@ -1,157 +1,61 @@
{
"type": "object",
"properties": {
"date": {
"type": "string"
},
"from": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"headers": {
"type": "object",
"properties": {
"arc-authentication-results": {
"type": "string"
},
"arc-message-signature": {
"type": "string"
},
"arc-seal": {
"type": "string"
},
"authentication-results": {
"type": "string"
},
"content-type": {
"type": "string"
},
"date": {
"type": "string"
},
"delivered-to": {
"type": "string"
},
"dkim-signature": {
"type": "string"
},
"from": {
"type": "string"
},
"message-id": {
"type": "string"
},
"mime-version": {
"type": "string"
},
"received": {
"type": "string"
},
"received-spf": {
"type": "string"
},
"return-path": {
"type": "string"
},
"subject": {
"type": "string"
},
"to": {
"type": "string"
},
"x-gm-features": {
"type": "string"
},
"x-gm-gg": {
"type": "string"
},
"x-gm-message-state": {
"type": "string"
},
"x-google-dkim-signature": {
"type": "string"
},
"x-google-smtp-source": {
"type": "string"
},
"x-received": {
"type": "string"
}
}
},
"id": {
"type": "string"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
}
},
"messageId": {
"type": "string"
},
"sizeEstimate": {
"type": "integer"
},
"subject": {
"type": "string"
},
"text": {
"type": "string"
},
"textAsHtml": {
"type": "string"
},
"threadId": {
"type": "string"
},
"to": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
}
},
"version": 2
"type": "object",
"properties": {
"Bcc": {
"type": "string"
},
"Cc": {
"type": "string"
},
"From": {
"type": "string"
},
"Subject": {
"type": "string"
},
"To": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"mimeType": {
"type": "string"
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"threadId": {
"type": "string"
}
},
"version": 1
}

View File

@ -0,0 +1,247 @@
{
"type": "object",
"properties": {
"bcc": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"cc": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"date": {
"type": "string"
},
"from": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"headers": {
"type": "object",
"properties": {
"arc-authentication-results": {
"type": "string"
},
"arc-message-signature": {
"type": "string"
},
"arc-seal": {
"type": "string"
},
"authentication-results": {
"type": "string"
},
"content-type": {
"type": "string"
},
"date": {
"type": "string"
},
"delivered-to": {
"type": "string"
},
"dkim-signature": {
"type": "string"
},
"from": {
"type": "string"
},
"message-id": {
"type": "string"
},
"mime-version": {
"type": "string"
},
"received": {
"type": "string"
},
"received-spf": {
"type": "string"
},
"return-path": {
"type": "string"
},
"subject": {
"type": "string"
},
"to": {
"type": "string"
},
"x-gm-features": {
"type": "string"
},
"x-gm-gg": {
"type": "string"
},
"x-gm-message-state": {
"type": "string"
},
"x-google-dkim-signature": {
"type": "string"
},
"x-google-smtp-source": {
"type": "string"
},
"x-received": {
"type": "string"
}
},
"additionalProperties": {
"type": "string"
}
},
"html": {
"type": ["string", "boolean"]
},
"id": {
"type": "string"
},
"inReplyTo": {
"type": "string"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
}
},
"messageId": {
"type": "string"
},
"references": {
"type": "array",
"items": {
"type": "string"
}
},
"replyTo": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"sizeEstimate": {
"type": "integer"
},
"subject": {
"type": "string"
},
"text": {
"type": "string"
},
"textAsHtml": {
"type": "string"
},
"threadId": {
"type": "string"
},
"to": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
}
},
"version": 2
}

View File

@ -1,55 +1,61 @@
{
"type": "object",
"properties": {
"From": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"mimeType": {
"type": "string"
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"Subject": {
"type": "string"
},
"threadId": {
"type": "string"
},
"To": {
"type": "string"
}
},
"version": 1
"type": "object",
"properties": {
"Bcc": {
"type": "string"
},
"Cc": {
"type": "string"
},
"From": {
"type": "string"
},
"Subject": {
"type": "string"
},
"To": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"mimeType": {
"type": "string"
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"threadId": {
"type": "string"
}
},
"version": 1
}

View File

@ -0,0 +1,247 @@
{
"type": "object",
"properties": {
"bcc": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"cc": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"date": {
"type": "string"
},
"from": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"headers": {
"type": "object",
"properties": {
"arc-authentication-results": {
"type": "string"
},
"arc-message-signature": {
"type": "string"
},
"arc-seal": {
"type": "string"
},
"authentication-results": {
"type": "string"
},
"content-type": {
"type": "string"
},
"date": {
"type": "string"
},
"delivered-to": {
"type": "string"
},
"dkim-signature": {
"type": "string"
},
"from": {
"type": "string"
},
"message-id": {
"type": "string"
},
"mime-version": {
"type": "string"
},
"received": {
"type": "string"
},
"received-spf": {
"type": "string"
},
"return-path": {
"type": "string"
},
"subject": {
"type": "string"
},
"to": {
"type": "string"
},
"x-gm-features": {
"type": "string"
},
"x-gm-gg": {
"type": "string"
},
"x-gm-message-state": {
"type": "string"
},
"x-google-dkim-signature": {
"type": "string"
},
"x-google-smtp-source": {
"type": "string"
},
"x-received": {
"type": "string"
}
},
"additionalProperties": {
"type": "string"
}
},
"html": {
"type": ["string", "boolean"]
},
"id": {
"type": "string"
},
"inReplyTo": {
"type": "string"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
}
},
"messageId": {
"type": "string"
},
"references": {
"type": "array",
"items": {
"type": "string"
}
},
"replyTo": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
},
"sizeEstimate": {
"type": "integer"
},
"subject": {
"type": "string"
},
"text": {
"type": "string"
},
"textAsHtml": {
"type": "string"
},
"threadId": {
"type": "string"
},
"to": {
"type": "object",
"properties": {
"html": {
"type": "string"
},
"text": {
"type": "string"
},
"value": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}
}
},
"version": 2
}

View File

@ -1,170 +1,124 @@
{
"type": "object",
"properties": {
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"ARC-Authentication-Results": {
"type": "string"
},
"ARC-Message-Signature": {
"type": "string"
},
"ARC-Seal": {
"type": "string"
},
"Authentication-Results": {
"type": "string"
},
"Content-Type": {
"type": "string"
},
"Date": {
"type": "string"
},
"Delivered-To": {
"type": "string"
},
"DKIM-Signature": {
"type": "string"
},
"From": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"Message-ID": {
"type": "string"
},
"MIME-Version": {
"type": "string"
},
"payload": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
}
}
}
}
}
},
"Received": {
"type": "string"
},
"Received-SPF": {
"type": "string"
},
"Return-Path": {
"type": "string"
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"Subject": {
"type": "string"
},
"threadId": {
"type": "string"
},
"To": {
"type": "string"
},
"X-Received": {
"type": "string"
}
}
}
}
},
"version": 3
"type": "object",
"properties": {
"Bcc": {
"type": "string"
},
"Cc": {
"type": "string"
},
"From": {
"type": "string"
},
"Subject": {
"type": "string"
},
"To": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"mimeType": {
"type": "string"
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"threadId": {
"type": "string"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Bcc": {
"type": "string"
},
"Cc": {
"type": "string"
},
"From": {
"type": "string"
},
"Subject": {
"type": "string"
},
"To": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"mimeType": {
"type": "string"
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"threadId": {
"type": "string"
}
}
}
}
},
"version": 1
}

View File

@ -0,0 +1,440 @@
{
"type": "object",
"properties": {
"Cc": {
"type": "string"
},
"Date": {
"type": "string"
},
"From": {
"type": "string"
},
"Subject": {
"type": "string"
},
"To": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"threadId": {
"type": "string"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Cc": {
"type": "string"
},
"Date": {
"type": "string"
},
"From": {
"type": "string"
},
"Subject": {
"type": "string"
},
"To": {
"type": "string"
},
"historyId": {
"type": "string"
},
"id": {
"type": "string"
},
"internalDate": {
"type": "string"
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"payload": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
},
"parts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"body": {
"type": "object",
"properties": {
"attachmentId": {
"type": "string"
},
"data": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"filename": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"mimeType": {
"type": "string"
},
"partId": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
},
"sizeEstimate": {
"type": "integer"
},
"snippet": {
"type": "string"
},
"threadId": {
"type": "string"
}
},
"additionalProperties": {
"type": "string"
}
}
}
},
"version": 1,
"additionalProperties": {
"type": "string"
}
}

View File

@ -0,0 +1,168 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
const schemaDir = path.join(__dirname, '..', '__schema__');
const readSchema = (version: string, file: string) => {
const content = readFileSync(path.join(schemaDir, version, 'message', file), 'utf-8');
return JSON.parse(content) as { properties: Record<string, unknown> };
};
describe.each(['v2.1.0', 'v2.2.0'])('Gmail message output schemas %s', (version) => {
describe.each(['get', 'getAll'])('%s', (operation) => {
it('describes the simplified output by default', () => {
const { properties } = readSchema(version, `${operation}.json`);
expect(properties).toHaveProperty('From');
expect(properties).toHaveProperty('Subject');
expect(properties).not.toHaveProperty('from');
});
it('describes the parsed email when Simplify is off', () => {
const { properties } = readSchema(version, `${operation}.simple-false.json`);
expect(properties).toHaveProperty('from');
expect(properties).toHaveProperty('headers');
expect(properties).toHaveProperty('text');
expect(properties).toHaveProperty('html');
expect(properties).toHaveProperty('textAsHtml');
expect(properties).not.toHaveProperty('From');
});
it('describes the address fields and thread headers of the parsed email', () => {
const { properties } = readSchema(version, `${operation}.simple-false.json`);
for (const key of ['cc', 'bcc', 'replyTo', 'references', 'inReplyTo']) {
expect(properties).toHaveProperty(key);
}
});
it('allows any header name in the parsed email headers', () => {
const { properties } = readSchema(version, `${operation}.simple-false.json`);
const headers = properties.headers as { additionalProperties?: { type?: string } };
expect(headers.additionalProperties?.type).toBe('string');
});
});
});
interface ThreadSchema {
properties: {
messages: {
items: {
properties: Record<string, SchemaNode>;
additionalProperties?: { type?: string };
};
};
};
}
interface SchemaNode {
type?: string | string[];
properties?: Record<string, SchemaNode>;
items?: SchemaNode;
}
const readThreadSchema = (version: string, file: string) =>
JSON.parse(readFileSync(path.join(schemaDir, version, 'thread', file), 'utf-8')) as ThreadSchema;
describe.each(['v2.1.0', 'v2.2.0'])('Gmail thread get output schemas %s', (version) => {
it('describes the simplified messages by default', () => {
const { properties } = readThreadSchema(version, 'get.json').properties.messages.items;
expect(Object.keys(properties).sort()).toEqual(
[
'Bcc',
'Cc',
'From',
'Subject',
'To',
'historyId',
'id',
'internalDate',
'labels',
'payload',
'sizeEstimate',
'snippet',
'threadId',
].sort(),
);
expect(Object.keys(properties.payload.properties ?? {})).toEqual(['mimeType']);
});
it('describes the full messages when Simplify is off', () => {
const { properties, additionalProperties } = readThreadSchema(version, 'get.simple-false.json')
.properties.messages.items;
const part = properties.payload.properties?.parts?.items;
expect(additionalProperties?.type).toBe('string');
expect(Object.keys(part?.properties ?? {})).toEqual(
expect.arrayContaining(['body', 'filename', 'headers', 'mimeType', 'partId', 'parts']),
);
expect(Object.keys(part?.properties?.body?.properties ?? {})).toEqual(
expect.arrayContaining(['data', 'size', 'attachmentId']),
);
});
it('does not describe parsed text fields, because the body stays encoded', () => {
const { properties } = readThreadSchema(version, 'get.simple-false.json').properties.messages
.items;
for (const key of ['text', 'html', 'textAsHtml']) {
expect(properties).not.toHaveProperty(key);
}
});
describe.each(['get.json', 'get.simple-false.json'])('%s', (file) => {
it('describes a message at the root, because the default returns one message for each item', () => {
const { properties } = readThreadSchema(version, file) as unknown as {
properties: Record<string, SchemaNode>;
};
expect(Object.keys(properties)).toEqual(
expect.arrayContaining(['From', 'Subject', 'snippet', 'payload', 'threadId']),
);
});
it('describes the thread envelope for Return Only Messages off', () => {
const { properties } = readThreadSchema(version, file) as unknown as {
properties: Record<string, SchemaNode>;
};
expect(properties.messages?.type).toBe('array');
expect(Object.keys(properties.messages?.items?.properties ?? {})).toEqual(
expect.arrayContaining(['From', 'snippet', 'payload']),
);
});
});
it('describes the attachment ID of the message body, at the root and in the envelope', () => {
const schema = readThreadSchema(version, 'get.simple-false.json') as unknown as {
properties: Record<string, SchemaNode> & { messages: { items: SchemaNode } };
};
const rootBody = schema.properties.payload?.properties?.body;
const envelopeBody = schema.properties.messages.items.properties?.payload?.properties?.body;
expect(Object.keys(rootBody?.properties ?? {})).toEqual(
expect.arrayContaining(['size', 'data', 'attachmentId']),
);
expect(Object.keys(envelopeBody?.properties ?? {})).toEqual(
expect.arrayContaining(['size', 'data', 'attachmentId']),
);
});
it('describes the nested parts of a full message down to three levels', () => {
const { properties } = readThreadSchema(version, 'get.simple-false.json').properties.messages
.items;
const level1 = properties.payload.properties?.parts?.items;
const level2 = level1?.properties?.parts?.items;
const level3 = level2?.properties?.parts?.items;
expect(Object.keys(level3?.properties ?? {})).toEqual(
expect.arrayContaining(['body', 'mimeType', 'partId']),
);
expect(Object.keys(level3?.properties?.body?.properties ?? {})).toEqual(
expect.arrayContaining(['data', 'attachmentId']),
);
});
});

View File

@ -0,0 +1,61 @@
import type { INodeProperties } from 'n8n-workflow';
import { GmailTrigger } from '../GmailTrigger.node';
import { messageFields } from '../v2/MessageDescription';
import { threadFields } from '../v2/ThreadDescription';
import { simplifyOutputShapeHint, threadSimplifyOutputShapeHint } from '../utils/descriptions';
const getSimpleHint = (properties: INodeProperties[], operation?: string) =>
properties.find(
(property) =>
property.name === 'simple' &&
(operation === undefined || property.displayOptions?.show?.operation?.includes(operation)),
)?.builderHint?.propertyHint;
describe('Gmail simplify output hint', () => {
it('names the sender field of both output shapes', () => {
expect(simplifyOutputShapeHint).toContain('$json.From');
expect(simplifyOutputShapeHint).toContain('$json.from.value[0].address');
});
it('tells the builder that `headers.from` is the full header line', () => {
expect(simplifyOutputShapeHint).toContain('full header line');
});
it('names attachments as a reason to turn Simplify off', () => {
expect(simplifyOutputShapeHint).toContain('attachments');
expect(simplifyOutputShapeHint).toContain('downloadAttachments');
});
it.each(['get', 'getAll'])('is the Simplify hint of the message %s operation', (operation) => {
expect(getSimpleHint(messageFields, operation)).toBe(simplifyOutputShapeHint);
});
it('is the Simplify hint of the trigger', () => {
const { properties } = new GmailTrigger().description;
expect(getSimpleHint(properties)).toBe(simplifyOutputShapeHint);
});
describe('thread get', () => {
it('is the Simplify hint of the thread get operation', () => {
expect(getSimpleHint(threadFields, 'get')).toBe(threadSimplifyOutputShapeHint);
});
it('says that both settings return the flat header keys', () => {
expect(threadSimplifyOutputShapeHint).toContain('`From`');
expect(threadSimplifyOutputShapeHint).toContain('Both settings');
});
it('says that Simplify off returns an encoded payload, not parsed text fields', () => {
expect(threadSimplifyOutputShapeHint).toContain('base64url');
expect(threadSimplifyOutputShapeHint).toContain('payload.parts');
expect(threadSimplifyOutputShapeHint).not.toContain('textAsHtml');
expect(threadSimplifyOutputShapeHint).toContain('no `text` or `html` field');
});
it('tells the builder to look through nested parts for the body', () => {
expect(threadSimplifyOutputShapeHint).toContain('nested `parts`');
});
});
});

View File

@ -10,3 +10,19 @@ export const simplifyMemoryNotice = ({
default: '',
displayOptions,
});
/**
* Tells the builder what each `simple` setting returns. The two settings use
* different field names for the sender, so code that reads the wrong one
* silently gets an empty value.
*/
export const simplifyOutputShapeHint =
'Keep true by default. True returns one flat item for each message. The sender is the string `$json.From`, for example "Sara Chen <sara@example.com>". The item also has `To`, `Cc`, `Bcc`, `Subject`, `snippet`, `labels`, `id` and `threadId`. It has no message body. False returns the parsed email. The sender is the object `$json.from`: read the address with `$json.from.value[0].address`, or parse the full header line `$json.headers.from`, for example "From: Sara Chen <sara@example.com>". The body is in `$json.text`, `$json.html` and `$json.textAsHtml`. With false, `$json.From` is undefined. With either setting, `$json.from` is not a string. To filter or route by sender, keep true. Set false only when the workflow needs the message body or the attachments, for example for AI classification or summarization. To get the attachments, also turn on the `downloadAttachments` option. False uses much more memory and often causes out-of-memory crashes.';
/**
* Tells the builder what each `simple` setting returns for a thread. Both
* settings flatten the headers. Simplify off does not parse the email: the body
* stays encoded in the Gmail API payload.
*/
export const threadSimplifyOutputShapeHint =
'Keep true by default. Both settings return the messages with the headers as flat keys: `From`, `To`, `Subject` and, when the message has them, `Cc` and `Bcc`. Both also return `snippet`, `labels`, `id` and `threadId`. True returns no message body. False also returns the Gmail API payload of each message. The body is in `payload.body.data` or in `payload.parts[].body.data`, encoded in base64url. In a nested multipart message the body is deeper: look through the nested `parts` until you find `body.data`. There is no `text` or `html` field, so decode the data in a Code node. Set false only when the workflow needs the message body or the attachments. False uses much more memory and often causes out-of-memory crashes.';

View File

@ -1,7 +1,7 @@
import { SEND_AND_WAIT_OPERATION, type INodeProperties } from 'n8n-workflow';
import { appendAttributionOption } from '../../../../utils/descriptions';
import { simplifyMemoryNotice } from '../utils/descriptions';
import { simplifyMemoryNotice, simplifyOutputShapeHint } from '../utils/descriptions';
export const messageOperations: INodeProperties[] = [
{
@ -301,8 +301,7 @@ export const messageFields: INodeProperties[] = [
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
builderHint: {
propertyHint:
'Keep true by default. When true, returns lightweight metadata (labels, subject, from, to, snippet). When false, fetches and parses the full raw email (adds html, text, textAsHtml, headers, attachments), which uses much more memory and is a common cause of out-of-memory crashes. Only set false when the email body is actually required, e.g. for AI classification, summarization, or content processing.',
propertyHint: simplifyOutputShapeHint,
},
},
simplifyMemoryNotice({
@ -390,8 +389,7 @@ export const messageFields: INodeProperties[] = [
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
builderHint: {
propertyHint:
'Keep true by default. When true, returns lightweight metadata (labels, subject, from, to, snippet) per message. When false, fetches and parses the full raw email for every message (adds html, text, textAsHtml, headers, attachments), which uses much more memory and is a common cause of out-of-memory crashes. Only set false when the email body is actually required, e.g. for AI classification, summarization, or content processing.',
propertyHint: simplifyOutputShapeHint,
},
},
simplifyMemoryNotice({

View File

@ -1,6 +1,6 @@
import type { INodeProperties } from 'n8n-workflow';
import { simplifyMemoryNotice } from '../utils/descriptions';
import { simplifyMemoryNotice, threadSimplifyOutputShapeHint } from '../utils/descriptions';
export const threadOperations: INodeProperties[] = [
{
@ -243,8 +243,7 @@ export const threadFields: INodeProperties[] = [
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
builderHint: {
propertyHint:
'Keep true by default. When true, returns lightweight metadata (labels, subject, from, to, snippet) for each message in the thread. When false, fetches and parses the full raw thread (adds html, text, textAsHtml, headers, attachments for every message), which uses much more memory and is a common cause of out-of-memory crashes. Only set false when the email body is actually required, e.g. for AI classification, summarization, or content processing.',
propertyHint: threadSimplifyOutputShapeHint,
},
},
simplifyMemoryNotice({

View File

@ -11,5 +11,42 @@
"type": "string"
}
},
"additionalProperties": {
"anyOf": [
{
"type": "string"
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"type": "null"
},
{
"type": "array",
"items": {
"type": ["string", "number", "boolean", "object"]
}
},
{
"type": "object",
"properties": {
"start": {
"type": "string"
},
"end": {
"type": ["string", "null"]
},
"time_zone": {
"type": ["string", "null"]
}
},
"required": ["start"]
}
]
},
"version": 1
}

View File

@ -0,0 +1,420 @@
{
"type": "object",
"properties": {
"object": {
"type": "string"
},
"id": {
"type": "string"
},
"created_time": {
"type": "string"
},
"last_edited_time": {
"type": "string"
},
"created_by": {
"type": "object",
"properties": {
"object": {
"type": "string"
},
"id": {
"type": "string"
}
}
},
"last_edited_by": {
"type": "object",
"properties": {
"object": {
"type": "string"
},
"id": {
"type": "string"
}
}
},
"cover": {
"type": ["object", "null"]
},
"icon": {
"type": ["object", "null"]
},
"parent": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"database_id": {
"type": "string"
},
"data_source_id": {
"type": "string"
}
}
},
"archived": {
"type": "boolean"
},
"in_trash": {
"type": "boolean"
},
"properties": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string"
},
"title": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"text": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"link": {
"type": ["object", "null"],
"properties": {
"url": {
"type": "string"
}
}
}
}
},
"annotations": {
"type": "object",
"properties": {
"bold": {
"type": "boolean"
},
"italic": {
"type": "boolean"
},
"strikethrough": {
"type": "boolean"
},
"underline": {
"type": "boolean"
},
"code": {
"type": "boolean"
},
"color": {
"type": "string"
}
}
},
"plain_text": {
"type": "string"
},
"href": {
"type": ["string", "null"]
}
}
}
},
"rich_text": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"text": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"link": {
"type": ["object", "null"],
"properties": {
"url": {
"type": "string"
}
}
}
}
},
"annotations": {
"type": "object",
"properties": {
"bold": {
"type": "boolean"
},
"italic": {
"type": "boolean"
},
"strikethrough": {
"type": "boolean"
},
"underline": {
"type": "boolean"
},
"code": {
"type": "boolean"
},
"color": {
"type": "string"
}
}
},
"plain_text": {
"type": "string"
},
"href": {
"type": ["string", "null"]
}
}
}
},
"number": {
"type": ["number", "null"]
},
"select": {
"anyOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"color": {
"type": "string"
}
}
},
{
"type": "null"
}
]
},
"status": {
"anyOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"color": {
"type": "string"
}
}
},
{
"type": "null"
}
]
},
"multi_select": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"color": {
"type": "string"
}
}
}
},
"date": {
"anyOf": [
{
"type": "object",
"properties": {
"start": {
"type": "string"
},
"end": {
"type": ["string", "null"]
},
"time_zone": {
"type": ["string", "null"]
}
},
"required": ["start"]
},
{
"type": "null"
}
]
},
"people": {
"type": "array",
"items": {
"type": "object",
"properties": {
"object": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"type": {
"type": "string"
},
"avatar_url": {
"type": ["string", "null"]
},
"person": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"email_verified": {
"type": "boolean"
}
}
}
}
}
},
"checkbox": {
"type": "boolean"
},
"url": {
"type": ["string", "null"]
},
"email": {
"type": ["string", "null"]
},
"phone_number": {
"type": ["string", "null"]
},
"relation": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}
},
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"type": {
"type": "string"
}
}
}
},
"formula": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
},
"rollup": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"function": {
"type": "string"
}
}
},
"unique_id": {
"type": "object",
"properties": {
"prefix": {
"type": ["string", "null"]
},
"number": {
"type": "number"
}
}
},
"created_time": {
"type": "string"
},
"last_edited_time": {
"type": "string"
},
"created_by": {
"type": "object",
"properties": {
"object": {
"type": "string"
},
"id": {
"type": "string"
}
}
},
"last_edited_by": {
"type": "object",
"properties": {
"object": {
"type": "string"
},
"id": {
"type": "string"
}
}
}
}
}
},
"url": {
"type": "string"
},
"public_url": {
"type": ["string", "null"]
},
"is_archived": {
"type": "boolean"
},
"is_locked": {
"type": "boolean"
},
"request_id": {
"type": "string"
}
},
"version": 1
}

View File

@ -11,5 +11,42 @@
"type": "string"
}
},
"additionalProperties": {
"anyOf": [
{
"type": "string"
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"type": "null"
},
{
"type": "array",
"items": {
"type": ["string", "number", "boolean", "object"]
}
},
{
"type": "object",
"properties": {
"start": {
"type": "string"
},
"end": {
"type": ["string", "null"]
},
"time_zone": {
"type": ["string", "null"]
}
},
"required": ["start"]
}
]
},
"version": 1
}

View File

@ -0,0 +1,420 @@
{
"type": "object",
"properties": {
"object": {
"type": "string"
},
"id": {
"type": "string"
},
"created_time": {
"type": "string"
},
"last_edited_time": {
"type": "string"
},
"created_by": {
"type": "object",
"properties": {
"object": {
"type": "string"
},
"id": {
"type": "string"
}
}
},
"last_edited_by": {
"type": "object",
"properties": {
"object": {
"type": "string"
},
"id": {
"type": "string"
}
}
},
"cover": {
"type": ["object", "null"]
},
"icon": {
"type": ["object", "null"]
},
"parent": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"database_id": {
"type": "string"
},
"data_source_id": {
"type": "string"
}
}
},
"archived": {
"type": "boolean"
},
"in_trash": {
"type": "boolean"
},
"properties": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string"
},
"title": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"text": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"link": {
"type": ["object", "null"],
"properties": {
"url": {
"type": "string"
}
}
}
}
},
"annotations": {
"type": "object",
"properties": {
"bold": {
"type": "boolean"
},
"italic": {
"type": "boolean"
},
"strikethrough": {
"type": "boolean"
},
"underline": {
"type": "boolean"
},
"code": {
"type": "boolean"
},
"color": {
"type": "string"
}
}
},
"plain_text": {
"type": "string"
},
"href": {
"type": ["string", "null"]
}
}
}
},
"rich_text": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"text": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"link": {
"type": ["object", "null"],
"properties": {
"url": {
"type": "string"
}
}
}
}
},
"annotations": {
"type": "object",
"properties": {
"bold": {
"type": "boolean"
},
"italic": {
"type": "boolean"
},
"strikethrough": {
"type": "boolean"
},
"underline": {
"type": "boolean"
},
"code": {
"type": "boolean"
},
"color": {
"type": "string"
}
}
},
"plain_text": {
"type": "string"
},
"href": {
"type": ["string", "null"]
}
}
}
},
"number": {
"type": ["number", "null"]
},
"select": {
"anyOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"color": {
"type": "string"
}
}
},
{
"type": "null"
}
]
},
"status": {
"anyOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"color": {
"type": "string"
}
}
},
{
"type": "null"
}
]
},
"multi_select": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"color": {
"type": "string"
}
}
}
},
"date": {
"anyOf": [
{
"type": "object",
"properties": {
"start": {
"type": "string"
},
"end": {
"type": ["string", "null"]
},
"time_zone": {
"type": ["string", "null"]
}
},
"required": ["start"]
},
{
"type": "null"
}
]
},
"people": {
"type": "array",
"items": {
"type": "object",
"properties": {
"object": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"type": {
"type": "string"
},
"avatar_url": {
"type": ["string", "null"]
},
"person": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"email_verified": {
"type": "boolean"
}
}
}
}
}
},
"checkbox": {
"type": "boolean"
},
"url": {
"type": ["string", "null"]
},
"email": {
"type": ["string", "null"]
},
"phone_number": {
"type": ["string", "null"]
},
"relation": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}
},
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"type": {
"type": "string"
}
}
}
},
"formula": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
},
"rollup": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"function": {
"type": "string"
}
}
},
"unique_id": {
"type": "object",
"properties": {
"prefix": {
"type": ["string", "null"]
},
"number": {
"type": "number"
}
}
},
"created_time": {
"type": "string"
},
"last_edited_time": {
"type": "string"
},
"created_by": {
"type": "object",
"properties": {
"object": {
"type": "string"
},
"id": {
"type": "string"
}
}
},
"last_edited_by": {
"type": "object",
"properties": {
"object": {
"type": "string"
},
"id": {
"type": "string"
}
}
}
}
}
},
"url": {
"type": "string"
},
"public_url": {
"type": ["string", "null"]
},
"is_archived": {
"type": "boolean"
},
"is_locked": {
"type": "boolean"
},
"request_id": {
"type": "string"
}
},
"version": 1
}

View File

@ -0,0 +1,178 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
interface PropertySchema {
type?: string | string[];
properties?: Record<string, PropertySchema>;
additionalProperties?: boolean | PropertySchema;
anyOf?: PropertySchema[];
items?: PropertySchema;
required?: string[];
}
const readSchema = (version: string, file: string): PropertySchema =>
JSON.parse(
readFileSync(path.join(__dirname, '..', '__schema__', version, 'databasePage', file), 'utf-8'),
) as PropertySchema;
const dateObjectOf = (schema: PropertySchema | boolean | undefined): PropertySchema | undefined =>
typeof schema === 'object'
? (schema.anyOf ?? [schema]).find(
(option) => option.type === 'object' && option.properties && 'start' in option.properties,
)
: undefined;
const hasDateObject = (schema: PropertySchema | boolean | undefined): boolean =>
typeof schema === 'object' &&
(schema.anyOf ?? [schema]).some(
(option) => option.type === 'object' && option.properties && 'start' in option.properties,
);
describe.each(['v3.0.0'])('Notion database page output schemas %s', (version) => {
describe.each(['get', 'getAll'])('%s', (operation) => {
it('describes the simplified output by default', () => {
const schema = readSchema(version, `${operation}.json`);
expect(Object.keys(schema.properties ?? {})).toEqual(['id', 'name', 'url']);
expect(schema.additionalProperties).toBeTruthy();
});
it('says that a simplified date value is an object with a start', () => {
const { additionalProperties } = readSchema(version, `${operation}.json`);
expect(hasDateObject(additionalProperties)).toBe(true);
});
it('describes the raw page when Simplify is off', () => {
const schema = readSchema(version, `${operation}.simple-false.json`);
expect(schema.properties).toHaveProperty('object');
expect(schema.properties).toHaveProperty('created_time');
expect(schema.properties).toHaveProperty('url');
expect(schema.properties).not.toHaveProperty('name');
});
it('accepts numbers and booleans in the array values of the simplified output', () => {
const { additionalProperties } = readSchema(version, `${operation}.json`);
const arrayOption =
typeof additionalProperties === 'object'
? additionalProperties.anyOf?.find((option) => option.type === 'array')
: undefined;
expect(arrayOption?.items?.type).toEqual(
expect.arrayContaining(['string', 'number', 'boolean', 'object']),
);
});
it('requires the start of a simplified date value', () => {
const { additionalProperties } = readSchema(version, `${operation}.json`);
expect(dateObjectOf(additionalProperties)?.required).toEqual(['start']);
});
it('requires the start of a raw date value', () => {
const schema = readSchema(version, `${operation}.simple-false.json`);
const columns = schema.properties?.properties?.additionalProperties;
expect(
dateObjectOf(typeof columns === 'object' ? columns.properties?.date : undefined)?.required,
).toEqual(['start']);
});
it('describes every field of a raw page', () => {
const { properties } = readSchema(version, `${operation}.simple-false.json`);
expect(Object.keys(properties ?? {})).toEqual(
expect.arrayContaining([
'object',
'id',
'created_time',
'last_edited_time',
'created_by',
'last_edited_by',
'cover',
'icon',
'parent',
'archived',
'in_trash',
'properties',
'url',
'public_url',
]),
);
});
it('describes the fields that the Notion API adds to a raw page', () => {
const { properties } = readSchema(version, `${operation}.simple-false.json`);
expect(Object.keys(properties ?? {})).toEqual(
expect.arrayContaining(['is_archived', 'is_locked', 'request_id']),
);
expect(properties?.parent?.properties).toHaveProperty('data_source_id');
});
it('describes the extra keys of people and text values in a raw page', () => {
const schema = readSchema(version, `${operation}.simple-false.json`);
const columns = schema.properties?.properties?.additionalProperties;
const columnProperties = typeof columns === 'object' ? columns.properties : undefined;
const person = columnProperties?.people?.items;
const text = columnProperties?.title?.items;
expect(Object.keys(person?.properties ?? {})).toEqual(
expect.arrayContaining(['object', 'type', 'avatar_url']),
);
expect(person?.properties?.person?.properties).toHaveProperty('email_verified');
expect(text?.properties?.text?.properties?.link?.properties).toHaveProperty('url');
expect(
columnProperties?.rich_text?.items?.properties?.text?.properties?.link?.properties,
).toHaveProperty('url');
expect(Object.keys(text?.properties ?? {})).toEqual(
expect.arrayContaining(['type', 'text', 'annotations', 'href']),
);
});
it('describes the value of every property type the node supports', () => {
const schema = readSchema(version, `${operation}.simple-false.json`);
const columns = schema.properties?.properties?.additionalProperties;
const columnProperties = typeof columns === 'object' ? columns.properties : undefined;
expect(Object.keys(columnProperties ?? {})).toEqual(
expect.arrayContaining([
'title',
'rich_text',
'number',
'select',
'status',
'multi_select',
'date',
'people',
'checkbox',
'url',
'email',
'phone_number',
'relation',
'files',
'formula',
'rollup',
'unique_id',
'created_time',
'last_edited_time',
'created_by',
'last_edited_by',
]),
);
});
it('types the raw page properties by column name, including dates and people', () => {
const schema = readSchema(version, `${operation}.simple-false.json`);
const columns = schema.properties?.properties?.additionalProperties;
expect(columns).toBeTruthy();
expect(typeof columns === 'object' && columns.properties).toHaveProperty('date');
expect(typeof columns === 'object' && columns.properties).toHaveProperty('people');
expect(
hasDateObject(typeof columns === 'object' ? columns.properties?.date : undefined),
).toBe(true);
});
});
});