import * as z from "zod" export const CELL_VALUE_SCHEMA = z.union([ z.string(), z.number(), z.boolean(), z.null(), ]) export const CELL_VALUES_SCHEMA = CELL_VALUE_SCHEMA.array().array() export const ROW_VALUES_SCHEMA = z.record(z.string(), CELL_VALUE_SCHEMA) export const ROW_VALUES_INPUT_SCHEMA = z.union([ ROW_VALUES_SCHEMA, ROW_VALUES_SCHEMA.array().min(1), ]) export const SPREADSHEET_REFERENCE_SCHEMA = z.string().trim().min(1) export const SHEET_REFERENCE_SCHEMA = z.union([ z.string().trim().min(1), z.number().int().nonnegative(), ]) export const TABLE_REFERENCE_SCHEMA = z.string().trim().min(1) export const MAJOR_DIMENSION_SCHEMA = z.enum(["rows", "columns"]) export const VALUE_INPUT_MODE_SCHEMA = z.enum(["userEntered", "raw"]) export const VALUE_RENDER_SCHEMA = z.enum([ "formatted", "unformatted", "formula", ]) export const DATE_TIME_RENDER_SCHEMA = z.enum([ "serialNumber", "formattedString", ]) export const AUTO_RECALC_SCHEMA = z.enum(["ON_CHANGE", "MINUTE", "HOUR"]) export const SHEET_TYPE_SCHEMA = z.enum(["GRID", "OBJECT", "DATA_SOURCE"]) export const HEX_COLOR_SCHEMA = z .templateLiteral(["#", z.string()]) .refine( (color) => /^#[0-9A-Fa-f]{6}$/.test(color), "Expected a six-digit hex color.", ) export const THEME_COLOR_SCHEMA = z.enum([ "text", "background", "accent1", "accent2", "accent3", "accent4", "accent5", "accent6", "link", ]) export const COLOR_SCHEMA = z.union([ HEX_COLOR_SCHEMA, z.strictObject({ /** Spreadsheet theme color that follows theme changes. */ theme: THEME_COLOR_SCHEMA, }), ]) export const ROW_SOURCE_SCHEMA = z.union([ z.object({ /** One-based row containing unique, non-empty column headers. */ headerRow: z.number().int().positive().optional(), /** Sheet title or immutable numeric sheet ID. */ sheet: SHEET_REFERENCE_SCHEMA, }), z.object({ /** Native Google Sheets table name or immutable table ID. */ table: TABLE_REFERENCE_SCHEMA, }), ]) export const GRID_RANGE_SCHEMA = z .object({ /** Inclusive one-based ending column. Omit for an unbounded range. */ endColumn: z.number().int().positive().optional(), /** Inclusive one-based ending row. Omit for an unbounded range. */ endRow: z.number().int().positive().optional(), /** Sheet title or immutable numeric sheet ID. */ sheet: SHEET_REFERENCE_SCHEMA, /** Inclusive one-based starting column. */ startColumn: z.number().int().positive().optional(), /** Inclusive one-based starting row. */ startRow: z.number().int().positive().optional(), }) .refine( ({ endColumn, startColumn }) => endColumn === undefined || startColumn === undefined || endColumn >= startColumn, "The ending column must not precede the starting column.", ) .refine( ({ endRow, startRow }) => endRow === undefined || startRow === undefined || endRow >= startRow, "The ending row must not precede the starting row.", ) export const RANGE_TARGET_SCHEMA = z.union([ GRID_RANGE_SCHEMA, z.strictObject({ /** Section of the native table to target. */ section: z.enum(["all", "header", "body", "footer"]).optional(), /** Native Google Sheets table name or immutable table ID. */ table: TABLE_REFERENCE_SCHEMA, }), ]) export const NORMALIZED_GRID_RANGE_SCHEMA = z.object({ /** Exclusive zero-based ending column. */ endColumnIndex: z.number().int().nonnegative().optional(), /** Exclusive zero-based ending row. */ endRowIndex: z.number().int().nonnegative().optional(), /** Immutable numeric sheet ID. */ sheetId: z.number().int().nonnegative(), /** Inclusive zero-based starting column. */ startColumnIndex: z.number().int().nonnegative().optional(), /** Inclusive zero-based starting row. */ startRowIndex: z.number().int().nonnegative().optional(), }) export const BORDER_STYLE_SCHEMA = z.enum([ "dotted", "dashed", "solid", "solidMedium", "solidThick", "double", ]) export const BORDER_SCHEMA = z.union([ z.null(), z.strictObject({ /** Border color. Omit to use the spreadsheet default. */ color: COLOR_SCHEMA.optional(), /** Provider-defined line style. */ style: BORDER_STYLE_SCHEMA, }), ]) export const BORDERS_SCHEMA = z .strictObject({ /** Bottom edge of the range. */ bottom: BORDER_SCHEMA.optional(), /** Horizontal lines inside the range. */ innerHorizontal: BORDER_SCHEMA.optional(), /** Vertical lines inside the range. */ innerVertical: BORDER_SCHEMA.optional(), /** Left edge of the range. */ left: BORDER_SCHEMA.optional(), /** Right edge of the range. */ right: BORDER_SCHEMA.optional(), /** Top edge of the range. */ top: BORDER_SCHEMA.optional(), }) .refine(hasDefinedValue, "At least one border must be supplied.") export const NUMBER_FORMAT_SCHEMA = z.strictObject({ /** Optional Google Sheets number-format pattern. */ pattern: z.string().optional(), /** Provider-defined number-format category. */ type: z.enum([ "text", "number", "percent", "currency", "date", "time", "dateTime", "scientific", ]), }) export const PADDING_SCHEMA = z.strictObject({ /** Bottom padding in pixels. */ bottom: z.number().int(), /** Left padding in pixels. */ left: z.number().int(), /** Right padding in pixels. */ right: z.number().int(), /** Top padding in pixels. */ top: z.number().int(), }) export const TEXT_ROTATION_SCHEMA = z.discriminatedUnion("type", [ z.strictObject({ /** Rotation angle from -90 through 90 degrees. */ angle: z.number().int().min(-90).max(90), type: z.literal("angle"), }), z.strictObject({ type: z.literal("vertical"), /** Whether text reads from top to bottom. */ vertical: z.boolean(), }), ]) export const TEXT_FORMAT_SCHEMA = z .strictObject({ /** Whether text is bold. */ bold: z.boolean().optional(), /** Text color. */ color: COLOR_SCHEMA.optional(), /** Font family name. */ family: z.string().optional(), /** Whether text is italic. */ italic: z.boolean().optional(), /** Cell-level link URI. */ link: z.string().optional(), /** Font size. */ size: z.number().int().optional(), /** Whether text is struck through. */ strikethrough: z.boolean().optional(), /** Whether text is underlined. */ underline: z.boolean().optional(), }) .refine( hasDefinedValue, "At least one text format property must be supplied.", ) export const CELL_FORMAT_SCHEMA = z .strictObject({ /** Cell background color. */ backgroundColor: COLOR_SCHEMA.optional(), /** Range edge and inner borders. */ borders: BORDERS_SCHEMA.optional(), /** Horizontal cell alignment. */ horizontalAlignment: z.enum(["left", "center", "right"]).optional(), /** Whether links render as links or plain text. */ hyperlinkDisplay: z.enum(["linked", "plainText"]).optional(), /** Google Sheets number format. */ numberFormat: NUMBER_FORMAT_SCHEMA.optional(), /** Cell padding. All four sides are required by Google. */ padding: PADDING_SCHEMA.optional(), /** Text rotation. */ rotation: TEXT_ROTATION_SCHEMA.optional(), /** Text direction. */ textDirection: z.enum(["leftToRight", "rightToLeft"]).optional(), /** Font and text styling. */ text: TEXT_FORMAT_SCHEMA.optional(), /** Vertical cell alignment. */ verticalAlignment: z.enum(["top", "middle", "bottom"]).optional(), /** Cell text wrapping behavior. */ wrap: z.enum(["overflow", "clip", "wrap"]).optional(), }) .refine(hasDefinedValue, "At least one format property must be supplied.") export const CLEAR_FORMAT_PROPERTY_SCHEMA = z.enum([ "backgroundColor", "borders", "horizontalAlignment", "hyperlinkDisplay", "numberFormat", "padding", "rotation", "text", "text.bold", "text.color", "text.family", "text.italic", "text.link", "text.size", "text.strikethrough", "text.underline", "textDirection", "verticalAlignment", "wrap", ]) export const TABLE_STYLE_SCHEMA = z.strictObject({ /** First alternating body-row color. */ firstBandColor: COLOR_SCHEMA.optional(), /** Footer color. Its presence means the table has a footer row. */ footerColor: COLOR_SCHEMA.optional(), /** Header row color. */ headerColor: COLOR_SCHEMA.optional(), /** Second alternating body-row color. */ secondBandColor: COLOR_SCHEMA.optional(), }) export const TABLE_STYLE_UPDATE_SCHEMA = z .strictObject({ /** New first band color, or null to restore the default. */ firstBandColor: COLOR_SCHEMA.nullable().optional(), /** New header color, or null to restore the default. */ headerColor: COLOR_SCHEMA.nullable().optional(), /** New second band color, or null to restore the default. */ secondBandColor: COLOR_SCHEMA.nullable().optional(), }) .refine( hasDefinedValue, "At least one table style property must be supplied.", ) export const BANDING_PROPERTIES_SCHEMA = z.strictObject({ /** Required first alternating color. */ firstBandColor: COLOR_SCHEMA, /** Optional final row or column color. */ footerColor: COLOR_SCHEMA.optional(), /** Optional first row or column color. */ headerColor: COLOR_SCHEMA.optional(), /** Required second alternating color. */ secondBandColor: COLOR_SCHEMA, }) export const BANDING_PROPERTIES_UPDATE_SCHEMA = z .strictObject({ /** New first band color. */ firstBandColor: COLOR_SCHEMA.optional(), /** New footer color, or null to clear it. */ footerColor: COLOR_SCHEMA.nullable().optional(), /** New header color, or null to clear it. */ headerColor: COLOR_SCHEMA.nullable().optional(), /** New second band color. */ secondBandColor: COLOR_SCHEMA.optional(), }) .refine(hasDefinedValue, "At least one banding property must be supplied.") export const BANDED_RANGE_SCHEMA = z .object({ /** Numeric ID accepted by update and delete requests, when supported. */ bandedRangeId: z.number().int().optional(), /** Column-by-column banding properties. */ columns: BANDING_PROPERTIES_SCHEMA.optional(), /** Provider reference for objects without a numeric ID. */ reference: z.string().optional(), /** Zero-based native Sheets grid range. */ range: NORMALIZED_GRID_RANGE_SCHEMA, /** Row-by-row banding properties. */ rows: BANDING_PROPERTIES_SCHEMA.optional(), }) .refine( ({ bandedRangeId, reference }) => bandedRangeId !== undefined || reference !== undefined, "A banded range must have an ID or provider reference.", ) .refine( ({ columns, rows }) => columns !== undefined || rows !== undefined, "A banded range must define row or column banding.", ) export const CONDITIONAL_FORMAT_SCHEMA = z .strictObject({ /** Background color applied when the condition matches. */ backgroundColor: COLOR_SCHEMA.optional(), /** Whether text is bold. */ bold: z.boolean().optional(), /** Whether text is italic. */ italic: z.boolean().optional(), /** Whether text is struck through. */ strikethrough: z.boolean().optional(), /** Text color applied when the condition matches. */ textColor: COLOR_SCHEMA.optional(), }) .refine( hasDefinedValue, "At least one conditional format property must be supplied.", ) export const RELATIVE_DATE_SCHEMA = z.enum([ "pastYear", "pastMonth", "pastWeek", "yesterday", "today", "tomorrow", ]) const SINGLE_VALUE_CONDITION_SCHEMA = z.strictObject({ type: z.enum([ "numberGreaterThan", "numberGreaterThanOrEqual", "numberLessThan", "numberLessThanOrEqual", "numberEqual", "numberNotEqual", "textContains", "textNotContains", "textStartsWith", "textEndsWith", "textEqual", "dateEqual", ]), value: z.string(), }) const BETWEEN_CONDITION_SCHEMA = z.strictObject({ type: z.enum(["numberBetween", "numberNotBetween"]), values: z.tuple([z.string(), z.string()]), }) const RELATIVE_DATE_CONDITION_SCHEMA = z.strictObject({ type: z.enum(["dateBefore", "dateAfter"]), value: z.union([ z.string(), z.strictObject({ relativeDate: RELATIVE_DATE_SCHEMA, }), ]), }) const EMPTY_CONDITION_SCHEMA = z.strictObject({ type: z.enum(["blank", "notBlank"]), }) const CUSTOM_FORMULA_CONDITION_SCHEMA = z.strictObject({ formula: z .string() .regex(/^[=+]/, "A custom formula must start with = or +."), type: z.literal("customFormula"), }) export const CONDITIONAL_FORMAT_CONDITION_SCHEMA = z.union([ SINGLE_VALUE_CONDITION_SCHEMA, BETWEEN_CONDITION_SCHEMA, RELATIVE_DATE_CONDITION_SCHEMA, EMPTY_CONDITION_SCHEMA, CUSTOM_FORMULA_CONDITION_SCHEMA, ]) const VALUE_INTERPOLATION_POINT_SCHEMA = z.strictObject({ color: COLOR_SCHEMA, type: z.enum(["number", "percent", "percentile"]), value: z.string(), }) export const MIN_INTERPOLATION_POINT_SCHEMA = z.union([ z.strictObject({ color: COLOR_SCHEMA, type: z.literal("min") }), VALUE_INTERPOLATION_POINT_SCHEMA, ]) export const MIDPOINT_INTERPOLATION_POINT_SCHEMA = VALUE_INTERPOLATION_POINT_SCHEMA export const MAX_INTERPOLATION_POINT_SCHEMA = z.union([ z.strictObject({ color: COLOR_SCHEMA, type: z.literal("max") }), VALUE_INTERPOLATION_POINT_SCHEMA, ]) export const CONDITIONAL_FORMAT_RULE_INPUT_SCHEMA = z.discriminatedUnion( "type", [ z.strictObject({ condition: CONDITIONAL_FORMAT_CONDITION_SCHEMA, format: CONDITIONAL_FORMAT_SCHEMA, type: z.literal("boolean"), }), z.strictObject({ max: MAX_INTERPOLATION_POINT_SCHEMA, midpoint: MIDPOINT_INTERPOLATION_POINT_SCHEMA.optional(), min: MIN_INTERPOLATION_POINT_SCHEMA, type: z.literal("gradient"), }), ], ) export const CONDITIONAL_FORMAT_RULE_SCHEMA = z.object({ /** Zero-based rule priority within its sheet. */ index: z.number().int().nonnegative(), /** Zero-based native ranges on one sheet. */ ranges: NORMALIZED_GRID_RANGE_SCHEMA.array().min(1), /** Boolean or gradient rule definition. */ rule: CONDITIONAL_FORMAT_RULE_INPUT_SCHEMA, }) export const TABLE_COLUMN_TYPE_SCHEMA = z.enum([ "unspecified", "number", "currency", "percent", "date", "time", "dateTime", "text", "boolean", "dropdown", "file", "person", "finance", "place", "rating", ]) export const TABLE_COLUMN_INPUT_SCHEMA = z .object({ /** Human-readable column header. */ name: z.string().trim().min(1), /** Dropdown choices; required only for dropdown columns. */ options: z.string().array().min(1).optional(), /** Native Google Sheets column type. */ type: TABLE_COLUMN_TYPE_SCHEMA.optional(), }) .refine( ({ options, type }) => (type === "dropdown") === Boolean(options?.length), "Dropdown columns require options, and other column types cannot have them.", ) export const TABLE_COLUMN_SCHEMA = z.object({ /** Zero-based position within the table. */ index: z.number().int().nonnegative(), /** Human-readable column header. */ name: z.string(), /** Dropdown choices configured for the column. */ options: z.string().array(), /** Native Google Sheets column type. */ type: TABLE_COLUMN_TYPE_SCHEMA, }) export const TABLE_SCHEMA = z.object({ /** Native table columns in display order. */ columns: TABLE_COLUMN_SCHEMA.array(), /** Native table name, unique within the spreadsheet. */ name: z.string(), /** Zero-based native Sheets grid range. */ range: NORMALIZED_GRID_RANGE_SCHEMA, /** Native header, body-band, and footer colors. */ style: TABLE_STYLE_SCHEMA, /** Immutable native table ID. */ tableId: z.string(), }) export const SHEET_SCHEMA = z.object({ /** Alternating-color ranges in the sheet. */ bandedRanges: BANDED_RANGE_SCHEMA.array(), /** Number of allocated columns, when the sheet uses a grid. */ columnCount: z.number().int().nonnegative().optional(), /** Conditional-format rules in zero-based priority order. */ conditionalFormats: CONDITIONAL_FORMAT_RULE_SCHEMA.array(), /** Number of frozen columns. */ frozenColumnCount: z.number().int().nonnegative(), /** Number of frozen rows. */ frozenRowCount: z.number().int().nonnegative(), /** Whether the sheet is hidden in the Google Sheets UI. */ hidden: z.boolean(), /** Zero-based position within the spreadsheet. */ index: z.number().int().nonnegative(), /** Whether the sheet lays out columns right-to-left. */ rightToLeft: z.boolean(), /** Number of allocated rows, when the sheet uses a grid. */ rowCount: z.number().int().nonnegative().optional(), /** Immutable numeric sheet ID. */ sheetId: z.number().int().nonnegative(), /** Google sheet type, such as `GRID` or `DATA_SOURCE`. */ sheetType: SHEET_TYPE_SCHEMA, /** Native tables contained in the sheet. */ tables: TABLE_SCHEMA.array(), /** Human-readable sheet title. */ title: z.string(), }) export const NAMED_RANGE_SCHEMA = z.object({ /** Immutable named-range ID. */ namedRangeId: z.string(), /** Human-readable range name. */ name: z.string(), /** Zero-based native Sheets grid range. */ range: NORMALIZED_GRID_RANGE_SCHEMA, }) export const SPREADSHEET_SCHEMA = z.object({ /** Recalculation policy for volatile formulas. */ autoRecalc: AUTO_RECALC_SCHEMA.optional(), /** Spreadsheet locale. */ locale: z.string().optional(), /** Named ranges defined in the spreadsheet. */ namedRanges: NAMED_RANGE_SCHEMA.array(), /** Sheets in display order. */ sheets: SHEET_SCHEMA.array(), /** Immutable spreadsheet ID. */ spreadsheetId: z.string(), /** Spreadsheet timezone. */ timeZone: z.string().optional(), /** Human-readable spreadsheet title. */ title: z.string(), /** Browser URL for the spreadsheet. */ url: z.string(), }) export const VALUE_RANGE_SCHEMA = z.object({ /** Dimension used by the outer values array. */ majorDimension: MAJOR_DIMENSION_SCHEMA, /** Provider-normalized A1 range. */ range: z.string(), /** Returned cell values. Trailing empty rows and columns are omitted. */ values: CELL_VALUES_SCHEMA, }) export const VALUE_UPDATE_SCHEMA = z.object({ /** Values after the update, when requested. */ values: VALUE_RANGE_SCHEMA.optional(), /** Number of updated cells. */ updatedCells: z.number().int().nonnegative(), /** Number of updated columns. */ updatedColumns: z.number().int().nonnegative(), /** Provider-normalized updated A1 range. */ updatedRange: z.string(), /** Number of updated rows. */ updatedRows: z.number().int().nonnegative(), }) export const BATCH_VALUE_UPDATE_SCHEMA = z.object({ /** Immutable spreadsheet ID. */ spreadsheetId: z.string(), /** Total number of updated cells. */ totalUpdatedCells: z.number().int().nonnegative(), /** Total number of updated columns. */ totalUpdatedColumns: z.number().int().nonnegative(), /** Total number of updated rows. */ totalUpdatedRows: z.number().int().nonnegative(), /** Total number of sheets receiving updates. */ totalUpdatedSheets: z.number().int().nonnegative(), /** Results corresponding to each requested update. */ updates: VALUE_UPDATE_SCHEMA.array(), }) export const APPEND_VALUES_RESULT_SCHEMA = VALUE_UPDATE_SCHEMA.extend({ /** Immutable spreadsheet ID. */ spreadsheetId: z.string(), /** Table range detected before values were appended. */ tableRange: z.string().optional(), }) export const SHEET_ROW_SCHEMA = z.object({ /** A1 range covering this row within the source columns. */ range: z.string(), /** One-based row number in the sheet. */ rowNumber: z.number().int().positive(), /** Values keyed by exact column header. */ values: ROW_VALUES_SCHEMA, }) export const ROW_MUTATION_RESULT_SCHEMA = z.object({ /** Number of cells affected by the mutation. */ affectedCells: z.number().int().nonnegative(), /** One-based row numbers targeted by the mutation. */ rowNumbers: z.number().int().positive().array(), /** Provider-normalized A1 range, when the provider supplies one. */ updatedRange: z.string().optional(), }) export const ROW_FILTER_SCHEMA = z.discriminatedUnion("operator", [ z.object({ column: z.string().min(1), operator: z.enum(["equals", "notEquals"]), value: CELL_VALUE_SCHEMA, }), z.object({ caseSensitive: z.boolean().optional(), column: z.string().min(1), operator: z.enum(["contains", "matchesRegex"]), value: z.string(), }), z.object({ column: z.string().min(1), operator: z.enum([ "greaterThan", "greaterThanOrEqual", "lessThan", "lessThanOrEqual", ]), value: z.number(), }), z.object({ column: z.string().min(1), operator: z.enum(["empty", "notEmpty", "isTrue", "isFalse"]), }), ]) /** * Reports whether an optional-property object contains a supplied value. * * @param values - Parsed optional-property object. */ function hasDefinedValue(values: Record) { return Object.values(values).some((value) => value !== undefined) }