Skip to content

Commit 351685e

Browse files
committed
Merge remote-tracking branch 'origin/staging'
2 parents 0f396fd + 9e135c5 commit 351685e

7 files changed

Lines changed: 352 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [3.5.0] - 2026-03-12
11+
12+
### Added
13+
14+
- **Optional `tableFactory` parameter in SchemaManager constructor** (SOSO-451)
15+
- New optional 3rd parameter: `tableFactory?: DynamicTableFactoryInterface`
16+
- When provided, the injected factory is used instead of creating one internally
17+
- Enables custom policies (e.g., `WriteConversionPolicy`, `UnknownFieldPolicy`) without unsafe property overrides
18+
- When not provided, behavior is identical to v3.4.0 (fully backward compatible)
19+
- Resolves OCP violation workaround in downstream MCP servers
20+
1021
## [3.1.0] - 2026-02-21
1122

1223
### Added

CLAUDE.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,10 @@ npx appsheet inspect --help # After npm install (uses bin entry)
124124
**SchemaManager** (`src/utils/SchemaManager.ts`)
125125

126126
- Central management class using factory injection (v3.0.0)
127-
- **v3.0.0 Constructor**: `new SchemaManager(clientFactory, schema)`
127+
- **v3.0.0 Constructor**: `new SchemaManager(clientFactory, schema, tableFactory?)`
128128
- `clientFactory`: AppSheetClientFactoryInterface (use AppSheetClientFactory or MockAppSheetClientFactory)
129129
- `schema`: SchemaConfig from SchemaLoader
130+
- `tableFactory` (optional): DynamicTableFactoryInterface — when provided, used instead of creating one internally. Enables custom policies (e.g., WriteConversionPolicy).
130131
- **`table<T>(connection, tableName, runAsUserEmail)`**: Creates DynamicTable instances on-the-fly
131132
- `runAsUserEmail` is required in v3.0.0 (not optional)
132133
- Each call creates a new client instance (lightweight operation)
@@ -367,6 +368,10 @@ await table.add([{ discount: 1.5 }]);
367368
const prodFactory = new AppSheetClientFactory();
368369
const prodDb = new SchemaManager(prodFactory, schema);
369370
371+
// Production with custom policies: Inject pre-configured DynamicTableFactory
372+
const tableFactory = new DynamicTableFactory(prodFactory, schema, undefined, writePolicy);
373+
const prodDbWithPolicy = new SchemaManager(prodFactory, schema, tableFactory);
374+
370375
// Testing: Use MockAppSheetClientFactory
371376
const testFactory = new MockAppSheetClientFactory(mockData);
372377
const testDb = new SchemaManager(testFactory, schema);
@@ -378,6 +383,7 @@ const testDb = new SchemaManager(testFactory, schema);
378383
- No need to mock axios or network calls
379384
- Test data can be pre-seeded via MockDataProvider
380385
- Same code paths for production and test environments
386+
- Custom DynamicTableFactory injection for policies (e.g., WriteConversionPolicy)
381387

382388
### Error Handling
383389

docs/SOSO-451/FEATURE_CONCEPT.md

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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 |

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@techdivision/appsheet",
3-
"version": "3.4.0",
3+
"version": "3.5.0",
44
"description": "Generic TypeScript library for AppSheet CRUD operations",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/utils/SchemaManager.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,27 @@ export class SchemaManager {
5252
*
5353
* @param clientFactory - Factory to create AppSheetClient instances
5454
* @param schema - Schema configuration containing connection and table definitions
55+
* @param tableFactory - Optional pre-configured DynamicTableFactory.
56+
* When provided, this factory is used instead of creating a new one internally.
57+
* Use this to inject factories with custom policies (e.g., WriteConversionPolicy).
5558
* @throws {ValidationError} If the schema is invalid
5659
*
5760
* @example
5861
* ```typescript
62+
* // Without custom factory (default behavior)
5963
* const factory = new AppSheetClientFactory();
6064
* const schema = SchemaLoader.fromYaml('./schema.yaml');
6165
* const db = new SchemaManager(factory, schema);
66+
*
67+
* // With custom factory (e.g., for locale-aware write conversion)
68+
* const tableFactory = new DynamicTableFactory(factory, schema, undefined, writePolicy);
69+
* const db = new SchemaManager(factory, schema, tableFactory);
6270
* ```
6371
*/
6472
constructor(
6573
clientFactory: AppSheetClientFactoryInterface,
66-
private readonly schema: SchemaConfig
74+
private readonly schema: SchemaConfig,
75+
tableFactory?: DynamicTableFactoryInterface
6776
) {
6877
// Validate schema
6978
const validation = SchemaLoader.validate(schema);
@@ -74,8 +83,8 @@ export class SchemaManager {
7483
);
7584
}
7685

77-
// Create table factory using injected client factory
78-
this.tableFactory = new DynamicTableFactory(clientFactory, schema);
86+
// Use injected factory or create default
87+
this.tableFactory = tableFactory ?? new DynamicTableFactory(clientFactory, schema);
7988
}
8089

8190
/**

0 commit comments

Comments
 (0)