|
1 | 1 | import { describe, it, expect } from 'vitest'; |
| 2 | +import { readFileSync } from 'node:fs'; |
| 3 | +import { dirname, resolve } from 'node:path'; |
| 4 | +import { fileURLToPath } from 'node:url'; |
2 | 5 | import { |
3 | 6 | FilterConditionSchema, |
4 | 7 | QueryFilterSchema, |
@@ -1834,3 +1837,92 @@ describe('FieldReferenceSchema.addDays (#14104)', () => { |
1834 | 1837 | }); |
1835 | 1838 | }); |
1836 | 1839 | }); |
| 1840 | + |
| 1841 | +// ============================================================================ |
| 1842 | +// [#16923] The docblock @examples are RUNNABLE, and same-table |
| 1843 | +// ============================================================================ |
| 1844 | + |
| 1845 | +/** |
| 1846 | + * [#16923] `FieldReferenceSchema`'s FIRST `@example` used to spell its `$field` |
| 1847 | + * comparand as the RELATION path `order.owner_id`, captioned as a join ON |
| 1848 | + * clause — while the same block's "Execution support" prose says a dotted path |
| 1849 | + * is refused by SQL push-down with `INVALID_FILTER`, and while `query.joins` |
| 1850 | + * (the only ON clause this protocol ever had) was removed in #4286. The |
| 1851 | + * example was the wrong half, established by RUNNING it rather than reading |
| 1852 | + * it: the schema door admits either spelling, so nothing here can be pinned by |
| 1853 | + * `safeParse` alone — the memory evaluator answers `false` for the dotted |
| 1854 | + * spelling on a flat row and the SQL compiler refuses it |
| 1855 | + * (`sql-driver-cross-field-reference.test.ts`, "a dotted relation path"). |
| 1856 | + * |
| 1857 | + * These pins hold the docblock to its own prose from two directions: |
| 1858 | + * |
| 1859 | + * - The `FieldReferenceSchema` block's examples PARSE, on both the |
| 1860 | + * documentation copy and the enforced copy — an example nobody can run is |
| 1861 | + * how the previous one drifted. |
| 1862 | + * - No `@example` ANYWHERE in `filter.zod.ts` spells a `$field` value as a |
| 1863 | + * path. The probe is on the CLAIM (what does an example say a comparand |
| 1864 | + * looks like), not on one spelling: it is case-insensitive, quote-agnostic, |
| 1865 | + * and refuses `.` and `/` alike, so a slash-separated or unbackticked |
| 1866 | + * respelling trips it too. |
| 1867 | + */ |
| 1868 | +describe('filter.zod.ts docblock @examples (#16923)', () => { |
| 1869 | + const HERE = dirname(fileURLToPath(import.meta.url)); |
| 1870 | + const SOURCE = readFileSync(resolve(HERE, 'filter.zod.ts'), 'utf8'); |
| 1871 | + |
| 1872 | + /** |
| 1873 | + * Every `@example` body in the file, as the raw comment text following the |
| 1874 | + * tag. An example body ends at the blank continuation line (` *`) that this |
| 1875 | + * file's convention puts after every one, or at the end of the docblock — |
| 1876 | + * so the body is the caption and the payload, never the prose after it. |
| 1877 | + */ |
| 1878 | + function exampleBlocks(source: string): string[] { |
| 1879 | + return source |
| 1880 | + .split('@example') |
| 1881 | + .slice(1) |
| 1882 | + .map((rest) => rest.split(/^[ \t]*\*[ \t]*$|\*\//m)[0]); |
| 1883 | + } |
| 1884 | + |
| 1885 | + /** The JSON payload lines of one `@example` body — the lines that are the example. */ |
| 1886 | + function payloads(block: string): string[] { |
| 1887 | + return block |
| 1888 | + .split('\n') |
| 1889 | + .map((line) => line.replace(/^\s*\*\s?/, '').trim()) |
| 1890 | + .filter((line) => line.startsWith('{')); |
| 1891 | + } |
| 1892 | + |
| 1893 | + /** Every `$field` VALUE inside a chunk of example text, any quote style, any case. */ |
| 1894 | + function fieldValues(text: string): string[] { |
| 1895 | + return [...text.matchAll(/["']?\$field["']?\s*:\s*["']([^"']*)["']/gi)].map((m) => m[1]); |
| 1896 | + } |
| 1897 | + |
| 1898 | + const blocks = exampleBlocks(SOURCE); |
| 1899 | + |
| 1900 | + it('lit control — the file really has @example blocks carrying $field values', () => { |
| 1901 | + // A zero below must mean "no path spellings", never "nothing was read". |
| 1902 | + expect(blocks.length).toBeGreaterThan(1); |
| 1903 | + expect(fieldValues(blocks.join('\n')).length).toBeGreaterThan(1); |
| 1904 | + // Dark control — a spelling that is not in the file returns nothing. |
| 1905 | + expect(fieldValues('{ "$fieldd": "order.owner_id" }')).toEqual([]); |
| 1906 | + }); |
| 1907 | + |
| 1908 | + it('the FieldReferenceSchema block\'s examples parse — documentation copy AND enforced copy', () => { |
| 1909 | + const block = SOURCE.slice(0, SOURCE.indexOf('export const FieldReferenceSchema')); |
| 1910 | + const examples = exampleBlocks(block).flatMap(payloads); |
| 1911 | + expect(examples.length).toBeGreaterThanOrEqual(2); |
| 1912 | + for (const line of examples) { |
| 1913 | + const value: unknown = JSON.parse(line); |
| 1914 | + expect(ComparisonOperatorSchema.safeParse(value).success, line).toBe(true); |
| 1915 | + expect(FieldOperatorsSchema.safeParse(value).success, line).toBe(true); |
| 1916 | + // …and the whole thing is a legal condition on a field, which is where an |
| 1917 | + // author copies it to. |
| 1918 | + expect(FilterConditionSchema.safeParse({ amount: value }).success, line).toBe(true); |
| 1919 | + } |
| 1920 | + }); |
| 1921 | + |
| 1922 | + it('no @example in the file spells a $field comparand as a path (dot OR slash)', () => { |
| 1923 | + const offenders = blocks |
| 1924 | + .flatMap((block) => fieldValues(block).map((value) => ({ block: block.trim().slice(0, 80), value }))) |
| 1925 | + .filter(({ value }) => /[./]/.test(value)); |
| 1926 | + expect(offenders, 'a $field comparand is a column of the SAME row').toEqual([]); |
| 1927 | + }); |
| 1928 | +}); |
0 commit comments