|
| 1 | +# SchemaManager: Optionaler tableFactory-Parameter |
| 2 | + |
| 3 | +## Status: Konzept |
| 4 | + |
| 5 | +| Feld | Wert | |
| 6 | +| ---------- | ----------------------------------------------------------- | |
| 7 | +| JIRA | SOSO-451 | |
| 8 | +| GitHub | #21 (Feature Request) | |
| 9 | +| Version | v3.5.0 (Minor — neuer optionaler Parameter) | |
| 10 | +| Abhaengig | Keine | |
| 11 | +| Betrifft | SchemaManager (`src/utils/SchemaManager.ts`) | |
| 12 | +| Prioritaet | Mittel — Workaround: unsicherer Property-Override existiert | |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +## Problemanalyse |
| 17 | + |
| 18 | +### Ausgangslage |
| 19 | + |
| 20 | +Der `SchemaManager`-Konstruktor (Zeile 64-78 in `src/utils/SchemaManager.ts`) erstellt |
| 21 | +seine `DynamicTableFactory` intern mit nur 2 Parametern: |
| 22 | + |
| 23 | +```typescript |
| 24 | +constructor( |
| 25 | + clientFactory: AppSheetClientFactoryInterface, |
| 26 | + private readonly schema: SchemaConfig |
| 27 | +) { |
| 28 | + // ... |
| 29 | + this.tableFactory = new DynamicTableFactory(clientFactory, schema); |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +### Konsequenz |
| 34 | + |
| 35 | +Consumer koennen keine vorkonfigurierte `DynamicTableFactory` mit Custom Policies |
| 36 | +uebergeben. Im `service_portfolio_mcp`-Projekt wird eine Factory mit |
| 37 | +`LocaleWriteConversionPolicy` fuer korrekte Datums-/Zahlenformatierung benoetigt. |
| 38 | + |
| 39 | +Aktueller Workaround (OCP-Verletzung): |
| 40 | + |
| 41 | +```typescript |
| 42 | +(schemaManager as unknown as { tableFactory: DynamicTableFactory }).tableFactory = tableFactory; |
| 43 | +``` |
| 44 | + |
| 45 | +Dieser Workaround: |
| 46 | + |
| 47 | +- Bricht bei internem Refactoring (private Property-Name aendert sich) |
| 48 | +- Umgeht TypeScript-Sicherheit (`unknown`-Cast) |
| 49 | +- Ist fuer Consumer nicht dokumentiert und nicht auffindbar |
| 50 | + |
| 51 | +--- |
| 52 | + |
| 53 | +## Loesung: Optionaler 3. Konstruktor-Parameter |
| 54 | + |
| 55 | +### Kernidee |
| 56 | + |
| 57 | +Der `SchemaManager`-Konstruktor erhaelt einen optionalen 3. Parameter |
| 58 | +`tableFactory?: DynamicTableFactoryInterface`. Wenn uebergeben, wird die |
| 59 | +injizierte Factory verwendet. Wenn nicht, wird wie bisher intern eine |
| 60 | +`DynamicTableFactory` erstellt. |
| 61 | + |
| 62 | +### Vorher (v3.4.0) |
| 63 | + |
| 64 | +```typescript |
| 65 | +const clientFactory = new AppSheetClientFactory(); |
| 66 | +const schema = SchemaLoader.fromYaml('./schema.yaml'); |
| 67 | + |
| 68 | +// Standard — keine Custom Policies moeglich |
| 69 | +const db = new SchemaManager(clientFactory, schema); |
| 70 | +``` |
| 71 | + |
| 72 | +### Nachher (v3.5.0) |
| 73 | + |
| 74 | +```typescript |
| 75 | +const clientFactory = new AppSheetClientFactory(); |
| 76 | +const schema = SchemaLoader.fromYaml('./schema.yaml'); |
| 77 | + |
| 78 | +// Option A: Ohne Factory (unveraendertes Verhalten) |
| 79 | +const db = new SchemaManager(clientFactory, schema); |
| 80 | + |
| 81 | +// Option B: Mit Custom Factory (z.B. fuer Locale-Konvertierung) |
| 82 | +const tableFactory = new DynamicTableFactory( |
| 83 | + clientFactory, |
| 84 | + schema, |
| 85 | + undefined, // unknownFieldPolicy |
| 86 | + new LocaleWriteConversionPolicy() // writeConversionPolicy |
| 87 | +); |
| 88 | +const dbWithLocale = new SchemaManager(clientFactory, schema, tableFactory); |
| 89 | +``` |
| 90 | + |
| 91 | +--- |
| 92 | + |
| 93 | +## Betroffene Dateien |
| 94 | + |
| 95 | +### `src/utils/SchemaManager.ts` |
| 96 | + |
| 97 | +**Konstruktor-Signatur erweitern:** |
| 98 | + |
| 99 | +```typescript |
| 100 | +constructor( |
| 101 | + clientFactory: AppSheetClientFactoryInterface, |
| 102 | + private readonly schema: SchemaConfig, |
| 103 | + tableFactory?: DynamicTableFactoryInterface // NEU: optional |
| 104 | +) { |
| 105 | + // Validate schema |
| 106 | + const validation = SchemaLoader.validate(schema); |
| 107 | + if (!validation.valid) { |
| 108 | + throw new ValidationError( |
| 109 | + `Invalid schema: ${validation.errors.join(', ')}`, |
| 110 | + validation.errors |
| 111 | + ); |
| 112 | + } |
| 113 | + |
| 114 | + // Use injected factory or create default |
| 115 | + this.tableFactory = tableFactory ?? new DynamicTableFactory(clientFactory, schema); |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +**Aenderung:** 1 Zeile Signatur, 1 Zeile Body (Fallback mit Nullish Coalescing). |
| 120 | + |
| 121 | +### TSDoc aktualisieren |
| 122 | + |
| 123 | +````typescript |
| 124 | +/** |
| 125 | + * Creates a new SchemaManager. |
| 126 | + * |
| 127 | + * @param clientFactory - Factory to create AppSheetClient instances |
| 128 | + * @param schema - Schema configuration containing connection and table definitions |
| 129 | + * @param tableFactory - Optional pre-configured DynamicTableFactory. |
| 130 | + * When provided, this factory is used instead of creating a new one internally. |
| 131 | + * Use this to inject factories with custom policies (e.g., WriteConversionPolicy). |
| 132 | + * @throws {ValidationError} If the schema is invalid |
| 133 | + * |
| 134 | + * @example |
| 135 | + * ```typescript |
| 136 | + * // Without custom factory (default behavior) |
| 137 | + * const db = new SchemaManager(clientFactory, schema); |
| 138 | + * |
| 139 | + * // With custom factory (e.g., for locale-aware write conversion) |
| 140 | + * const tableFactory = new DynamicTableFactory(clientFactory, schema, undefined, writePolicy); |
| 141 | + * const db = new SchemaManager(clientFactory, schema, tableFactory); |
| 142 | + * ``` |
| 143 | + */ |
| 144 | +```` |
| 145 | + |
| 146 | +--- |
| 147 | + |
| 148 | +## Abwaertskompatibilitaet |
| 149 | + |
| 150 | +| Aspekt | Bewertung | Begruendung | |
| 151 | +| --------------------- | ------------- | ------------------------------------------------------ | |
| 152 | +| Bestehende Aufrufe | Kompatibel | Parameter ist optional, Default-Verhalten unveraendert | |
| 153 | +| API-Kontrakt | Kompatibel | Additive Aenderung (neuer optionaler Parameter) | |
| 154 | +| Semver-Einstufung | Minor (3.5.0) | Feature-Addition ohne Breaking Change | |
| 155 | +| TypeScript-Interfaces | Kompatibel | Keine Interface-Aenderung noetig | |
| 156 | + |
| 157 | +**Kein Consumer muss Code aendern.** Bestehende Aufrufe mit 2 Parametern |
| 158 | +funktionieren identisch. |
| 159 | + |
| 160 | +--- |
| 161 | + |
| 162 | +## Test-Strategie |
| 163 | + |
| 164 | +### Unit-Tests: Konstruktor-Pfade |
| 165 | + |
| 166 | +```typescript |
| 167 | +describe('SchemaManager constructor', () => { |
| 168 | + describe('without tableFactory (default behavior)', () => { |
| 169 | + it('should create internal DynamicTableFactory', () => { |
| 170 | + const db = new SchemaManager(clientFactory, validSchema); |
| 171 | + // Verify table() works (implicitly tests internal factory creation) |
| 172 | + const table = db.table('default', 'users', 'user@example.com'); |
| 173 | + expect(table).toBeInstanceOf(DynamicTable); |
| 174 | + }); |
| 175 | + }); |
| 176 | + |
| 177 | + describe('with injected tableFactory', () => { |
| 178 | + it('should use the provided factory instead of creating a new one', () => { |
| 179 | + const mockTableFactory: DynamicTableFactoryInterface = { |
| 180 | + create: jest.fn().mockReturnValue(mockDynamicTable), |
| 181 | + }; |
| 182 | + |
| 183 | + const db = new SchemaManager(clientFactory, validSchema, mockTableFactory); |
| 184 | + db.table('default', 'users', 'user@example.com'); |
| 185 | + |
| 186 | + expect(mockTableFactory.create).toHaveBeenCalledWith('default', 'users', 'user@example.com'); |
| 187 | + }); |
| 188 | + |
| 189 | + it('should not create a DynamicTableFactory when one is provided', () => { |
| 190 | + const spy = jest.spyOn(DynamicTableFactory.prototype, 'create'); |
| 191 | + const customFactory: DynamicTableFactoryInterface = { |
| 192 | + create: jest.fn().mockReturnValue(mockDynamicTable), |
| 193 | + }; |
| 194 | + |
| 195 | + new SchemaManager(clientFactory, validSchema, customFactory); |
| 196 | + |
| 197 | + // Verify the internal factory's create() is never called |
| 198 | + expect(spy).not.toHaveBeenCalled(); |
| 199 | + spy.mockRestore(); |
| 200 | + }); |
| 201 | + }); |
| 202 | + |
| 203 | + describe('schema validation still applies', () => { |
| 204 | + it('should throw ValidationError for invalid schema even with custom factory', () => { |
| 205 | + const customFactory: DynamicTableFactoryInterface = { |
| 206 | + create: jest.fn(), |
| 207 | + }; |
| 208 | + |
| 209 | + expect(() => { |
| 210 | + new SchemaManager(clientFactory, invalidSchema, customFactory); |
| 211 | + }).toThrow(ValidationError); |
| 212 | + }); |
| 213 | + }); |
| 214 | +}); |
| 215 | +``` |
| 216 | + |
| 217 | +### Integration-Test: Custom Policy durchreichen |
| 218 | + |
| 219 | +```typescript |
| 220 | +describe('SchemaManager with custom WriteConversionPolicy', () => { |
| 221 | + it('should use locale-aware conversion when custom factory is provided', async () => { |
| 222 | + const writePolicy = new LocaleWriteConversionPolicy('de-DE'); |
| 223 | + const tableFactory = new DynamicTableFactory(mockClientFactory, schema, undefined, writePolicy); |
| 224 | + |
| 225 | + const db = new SchemaManager(mockClientFactory, schema, tableFactory); |
| 226 | + const table = db.table('default', 'worklogs', 'user@example.com'); |
| 227 | + |
| 228 | + // Verify that write operations use the locale policy |
| 229 | + await table.add([{ date: '2026-03-12' }]); |
| 230 | + // Assert that the date was converted to de-DE format before API call |
| 231 | + }); |
| 232 | +}); |
| 233 | +``` |
| 234 | + |
| 235 | +--- |
| 236 | + |
| 237 | +## Implementierungsplan |
| 238 | + |
| 239 | +| Phase | Aufwand | Beschreibung | |
| 240 | +| --------------- | --------- | ------------------------------------------------- | |
| 241 | +| 1. Konstruktor | 0.25h | Optionalen Parameter + Fallback-Logik hinzufuegen | |
| 242 | +| 2. TSDoc | 0.25h | Dokumentation aktualisieren | |
| 243 | +| 3. Unit-Tests | 0.5h | Tests fuer beide Pfade + Validation | |
| 244 | +| 4. Version Bump | 0.1h | `package.json` auf 3.5.0 | |
| 245 | +| 5. AGENTS.md | 0.25h | Dokumentation in AGENTS.md aktualisieren | |
| 246 | +| **Gesamt** | **~1.5h** | | |
| 247 | + |
| 248 | +--- |
| 249 | + |
| 250 | +## Risikobewertung |
| 251 | + |
| 252 | +| Risiko | Einstufung | Mitigation | |
| 253 | +| ------------------------------- | ----------- | ------------------------------------------------ | |
| 254 | +| Breaking Change | Kein Risiko | Neuer optionaler Parameter, Default unveraendert | |
| 255 | +| Schema-Validation wird umgangen | Kein Risiko | Validation laeuft vor Factory-Zuweisung | |
| 256 | +| Inkonsistente Factory/Schema | Niedrig | Consumer-Verantwortung, dokumentiert in TSDoc | |
0 commit comments