-
Notifications
You must be signed in to change notification settings - Fork 0
feat(db): Phase 2c-i — style_templates + style_rules + default style_rules seed #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
c94f5c4
feat(db): Phase 2c-i — style_templates + style_rules + default seed
thewrz 4f569b2
refactor(db): enforce StyleNodeType union + CHECK constraints on styl…
thewrz 9b617e3
refactor(db): reject empty / whitespace-only style_template names
thewrz 0170398
Merge remote-tracking branch 'origin/main' into feat/issue-30
thewrz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import type { MigrationBuilder } from 'node-pg-migrate'; | ||
|
|
||
| export const up = (pgm: MigrationBuilder): void => { | ||
| pgm.createTable('style_templates', { | ||
| id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, | ||
| name: { type: 'text', notNull: true, unique: true }, | ||
| owner: { type: 'text' }, | ||
| created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') }, | ||
| }); | ||
| // Reject empty / whitespace-only template names — semantically invalid and | ||
| // would silently break getTemplateByName() lookups. | ||
| pgm.addConstraint('style_templates', 'style_templates_name_non_empty_check', { | ||
| check: `length(trim(name)) > 0`, | ||
| }); | ||
|
|
||
| pgm.createTable('style_rules', { | ||
| id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') }, | ||
| template_id: { | ||
| type: 'uuid', | ||
| notNull: true, | ||
| references: 'style_templates', | ||
| onDelete: 'CASCADE', | ||
| }, | ||
| // 'part' | 'article' | 'pr1' | 'pr2' | 'pr3' | 'pr4' | 'pr5' | ||
| node_type: { type: 'varchar(20)', notNull: true }, | ||
| font_family: { type: 'text' }, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| // OOXML native unit: half-points (20 = 10pt) | ||
| font_size_half_pt: { type: 'integer' }, | ||
| bold: { type: 'boolean', notNull: true, default: false }, | ||
| caps: { type: 'boolean', notNull: true, default: false }, | ||
| // OOXML native unit: twips (1440 twips = 1 inch) | ||
| indent_twips: { type: 'integer' }, | ||
| space_before_twips: { type: 'integer' }, | ||
| space_after_twips: { type: 'integer' }, | ||
| numbering_format: { type: 'text' }, | ||
| }); | ||
|
|
||
| pgm.addConstraint('style_rules', 'style_rules_template_node_type_key', { | ||
| unique: ['template_id', 'node_type'], | ||
| }); | ||
| // Enforce the StyleNodeType domain at the DB boundary — mirrors the TS union | ||
| // exported from src/db/queries/templates.ts. Keep the two in sync. | ||
| pgm.addConstraint('style_rules', 'style_rules_node_type_check', { | ||
| check: `node_type IN ('part', 'article', 'pr1', 'pr2', 'pr3', 'pr4', 'pr5')`, | ||
| }); | ||
| // OOXML units are unsigned by definition: half-points and twips cannot be negative. | ||
| // Defends against bad data from future template imports / API writes. | ||
| pgm.addConstraint('style_rules', 'style_rules_non_negative_ooxml_units_check', { | ||
| check: ` | ||
| (font_size_half_pt IS NULL OR font_size_half_pt >= 0) AND | ||
| (indent_twips IS NULL OR indent_twips >= 0) AND | ||
| (space_before_twips IS NULL OR space_before_twips >= 0) AND | ||
| (space_after_twips IS NULL OR space_after_twips >= 0) | ||
| `, | ||
| }); | ||
| pgm.createIndex('style_rules', 'template_id', { name: 'style_rules_template_idx' }); | ||
| }; | ||
|
|
||
| export const down = (pgm: MigrationBuilder): void => { | ||
| pgm.dropTable('style_rules', { cascade: true }); | ||
| pgm.dropTable('style_templates', { cascade: true }); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import type { MigrationBuilder } from 'node-pg-migrate'; | ||
|
|
||
| /** | ||
| * Default UFGS-derived style template seed. | ||
| * | ||
| * Source fixture: docs/references/UFGS/DIVISION_27/27080001.docx (gitignored) | ||
| * Extraction commands (re-run to verify): | ||
| * unzip -p docs/references/UFGS/DIVISION_27/27080001.docx word/styles.xml > /tmp/specr-styles.xml | ||
| * unzip -p docs/references/UFGS/DIVISION_27/27080001.docx word/numbering.xml > /tmp/specr-numbering.xml | ||
| * | ||
| * Resolved values (basedOn chain + numId=2 abstractNum lookup): | ||
| * | ||
| * Normal style (root): | ||
| * rFonts ascii="Courier New", sz=20 (half-pt → 10pt), spacing before=0 after=120 | ||
| * SpecNormal (basedOn Normal): | ||
| * adds spacing line=360 before=0 after=0 (overrides Normal spacing) | ||
| * | ||
| * | NodeType | Style | basedOn chain | numPr (ilvl/numId) | abstractNum ilvl source | Font | sz | bold | caps | ind start | before | after | lvlText | | ||
| * |----------|----------|----------------------|--------------------|-------------------------|-------------|----|------|------|-----------|--------|-------|------------| | ||
| * | part | PART | PART→Normal | ilvl=0, numId=2 | ilvl=0 | Courier New | 20 | true | true | 0 | 0 | 120 | PART %1 - | | ||
| * | article | Article | Article→Normal | none (parent-only) | ilvl=1 | Courier New | 20 | false| true | NULL | 0 | 120 | %1.%2 | | ||
| * | pr1 | Level11 | Level11→SpecNormal→N | ilvl=2, numId=2 | ilvl=2 | Courier New | 20 | false| false| 720 | 0 | 0 | %3. | | ||
| * | pr2 | Level21 | Level21→Level11→… | inherits ilvl=2 | ilvl=3 (per design map) | Courier New | 20 | false| false| 1080 | 0 | 0 | %4. | | ||
| * | pr3 | Level31 | Level31→Level21→… | inherits | ilvl=4 | Courier New | 20 | false| false| 1440 | 0 | 0 | %5. | | ||
| * | pr4 | Level41 | Level41→Level31→… | inherits | ilvl=5 | Courier New | 20 | false| false| 1800 | 0 | 0 | %6) | | ||
| * | pr5 | Level51 | Level51→Level41→… | inherits | ilvl=6 | Courier New | 20 | false| false| 2160 | 0 | 0 | %7) | | ||
| * | ||
| * lvlText values normalized to match buildSpecNumberingConfig() (no trailing spaces); | ||
| * the UFGS fixture has trailing spaces on ilvl=0 ("PART %1 - ") and ilvl=1 ("%1.%2 ") | ||
| * but our generator emits the trimmed form. See issue #30 for rationale. | ||
| * | ||
| * Indent values come from the abstractNum level override (per design doc methodology: | ||
| * "abstractNum level override → paragraph style → null"). Spacing values come from | ||
| * the paragraph style basedOn chain (Normal vs SpecNormal). Where extraction yielded | ||
| * no value, we write NULL — never fabricate. | ||
| */ | ||
|
|
||
| type SeedNodeType = 'part' | 'article' | 'pr1' | 'pr2' | 'pr3' | 'pr4' | 'pr5'; | ||
|
|
||
| interface SeedRow { | ||
| readonly nodeType: SeedNodeType; | ||
| readonly fontFamily: string | null; | ||
| readonly fontSizeHalfPt: number | null; | ||
| readonly bold: boolean; | ||
| readonly caps: boolean; | ||
| readonly indentTwips: number | null; | ||
| readonly spaceBeforeTwips: number | null; | ||
| readonly spaceAfterTwips: number | null; | ||
| readonly numberingFormat: string | null; | ||
| } | ||
|
|
||
| const ROWS: readonly SeedRow[] = [ | ||
| cn('part', true, true, 0, 0, 120, 'PART %1 -'), | ||
| cn('article', false, true, null, 0, 120, '%1.%2'), | ||
| cn('pr1', false, false, 720, 0, 0, '%3.'), | ||
| cn('pr2', false, false, 1080, 0, 0, '%4.'), | ||
| cn('pr3', false, false, 1440, 0, 0, '%5.'), | ||
| cn('pr4', false, false, 1800, 0, 0, '%6)'), | ||
| cn('pr5', false, false, 2160, 0, 0, '%7)'), | ||
| ]; | ||
|
|
||
| function cn( | ||
| nodeType: SeedNodeType, | ||
| bold: boolean, | ||
| caps: boolean, | ||
| indent: number | null, | ||
| before: number | null, | ||
| after: number | null, | ||
| fmt: string | ||
| ): SeedRow { | ||
| return { | ||
| nodeType, | ||
| fontFamily: 'Courier New', | ||
| fontSizeHalfPt: 20, | ||
| bold, | ||
| caps, | ||
| indentTwips: indent, | ||
| spaceBeforeTwips: before, | ||
| spaceAfterTwips: after, | ||
| numberingFormat: fmt, | ||
| }; | ||
| } | ||
|
|
||
| function sqlLit(v: string | number | boolean | null): string { | ||
| if (v === null) return 'NULL'; | ||
| if (typeof v === 'number') return String(v); | ||
| if (typeof v === 'boolean') return v ? 'true' : 'false'; | ||
| // Escape single quotes in string literals (defense in depth — seed values are static) | ||
| return `'${v.replace(/'/g, "''")}'`; | ||
| } | ||
|
|
||
| function insertSql(row: SeedRow): string { | ||
| const cols = [ | ||
| row.nodeType, | ||
| row.fontFamily, | ||
| row.fontSizeHalfPt, | ||
| row.bold, | ||
| row.caps, | ||
| row.indentTwips, | ||
| row.spaceBeforeTwips, | ||
| row.spaceAfterTwips, | ||
| row.numberingFormat, | ||
| ]; | ||
| const literals = cols.map(sqlLit).join(', '); | ||
| return `INSERT INTO style_rules ( | ||
| template_id, node_type, font_family, font_size_half_pt, | ||
| bold, caps, indent_twips, space_before_twips, space_after_twips, numbering_format | ||
| ) SELECT id, ${literals} | ||
| FROM style_templates WHERE name = 'UFGS-Default'`; | ||
| } | ||
|
|
||
| export const up = (pgm: MigrationBuilder): void => { | ||
| pgm.sql(`INSERT INTO style_templates (name, owner) VALUES ('UFGS-Default', NULL)`); | ||
| for (const row of ROWS) { | ||
| pgm.sql(insertSql(row)); | ||
| } | ||
| }; | ||
|
|
||
| export const down = (pgm: MigrationBuilder): void => { | ||
| const nodeTypeList = ROWS.map((r) => `'${r.nodeType}'`).join(','); | ||
| pgm.sql( | ||
| `DELETE FROM style_rules WHERE template_id IN (SELECT id FROM style_templates WHERE name = 'UFGS-Default') AND node_type IN (${nodeTypeList})` | ||
| ); | ||
| pgm.sql(`DELETE FROM style_templates WHERE name = 'UFGS-Default'`); | ||
| }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.