@pattern-stack/codegen 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pattern-stack/codegen",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Entity-driven code generation for full-stack TypeScript applications",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -80,6 +80,7 @@ export {
80
80
  export type {
81
81
  IncrementalRead,
82
82
  RandomRead,
83
+ ReadContext,
83
84
  ReadMode,
84
85
  ReadRequest,
85
86
  Ref,
@@ -79,6 +79,26 @@ export interface ReadRequest<F = unknown> {
79
79
  readonly pageSize?: number;
80
80
  }
81
81
 
82
+ /**
83
+ * Per-run context threaded from `listChanges` into the vendor read body (R5).
84
+ *
85
+ * Carries the `subscription` framing the run so `enumerate`/`hydrate` can resolve
86
+ * **per-connection credentials** (and raw-landing keys) from
87
+ * `subscription.externalRef` — the gap a multi-account consumer surfaced: a
88
+ * singleton change source cannot hold connection-scoped auth, and before R5 the
89
+ * base forwarded the subscription only into `filterFor`, never into the fetch.
90
+ *
91
+ * Optional throughout (the core contract): a direct `read()` / `get()` call — the
92
+ * query surface's "fill one record on click" — may omit it. An adapter that needs
93
+ * per-connection auth reads `ctx?.subscription?.externalRef` and asserts its
94
+ * presence; a provider-level-auth adapter ignores it.
95
+ */
96
+ export interface ReadContext {
97
+ /** The subscription framing this run; `externalRef` is the upstream scope /
98
+ * connection id the adapter resolves credentials + raw-landing keys from. */
99
+ readonly subscription?: IntegrationSubscriptionView;
100
+ }
101
+
82
102
  /**
83
103
  * The `read()`-side envelope: canonical record + the raw vendor payload it came
84
104
  * from + the originating external id + the per-ref cursor.
@@ -101,7 +121,7 @@ export interface SourcedRecord<T> {
101
121
  * hydration, and cursor emission are the providing base's concern.
102
122
  */
103
123
  export interface IncrementalRead<T, F = unknown> {
104
- read(req: ReadRequest<F>): AsyncIterable<SourcedRecord<T>>;
124
+ read(req: ReadRequest<F>, ctx?: ReadContext): AsyncIterable<SourcedRecord<T>>;
105
125
  }
106
126
 
107
127
  /**
@@ -111,7 +131,7 @@ export interface IncrementalRead<T, F = unknown> {
111
131
  * surface.
112
132
  */
113
133
  export interface RandomRead<T> {
114
- get(id: string): Promise<T | null>;
134
+ get(id: string, ctx?: ReadContext): Promise<T | null>;
115
135
  }
116
136
 
117
137
  // ============================================================================
@@ -208,11 +228,16 @@ export abstract class IncrementalReadBase<T, F = unknown, M = Record<string, unk
208
228
  * `pageSize` (from `ReadRequest`) is the adapter's requested vendor page size
209
229
  * — also the atomic-cursor backfill blast-radius bound (§ `cursorDivisible`).
210
230
  * Honor it as a hint; vendors that cap page size clamp it.
231
+ *
232
+ * `ctx?.subscription` (R5) carries the run's subscription, so a per-connection
233
+ * adapter resolves credentials / upstream scope from `externalRef` here; absent
234
+ * on a direct `read()` with no run subscription.
211
235
  */
212
236
  protected abstract enumerate(
213
237
  mode: ReadMode,
214
238
  filter?: F,
215
239
  pageSize?: number,
240
+ ctx?: ReadContext,
216
241
  ): AsyncIterable<Ref<M>[]>;
217
242
 
218
243
  /**
@@ -221,8 +246,12 @@ export abstract class IncrementalReadBase<T, F = unknown, M = Record<string, unk
221
246
  * alignment. Write it over `mapConcurrent(ids, (id) => this.fetchOne(id),
222
247
  * this.hydrateConcurrency)`; override with a real `/batch` call or a
223
248
  * passthrough (full-object list) where the vendor allows.
249
+ *
250
+ * `ctx?.subscription` (R5) carries the run's subscription for per-connection
251
+ * credential resolution (the fetch is where the vendor call happens) and is the
252
+ * natural place to land raw payloads keyed by `subscription.id`.
224
253
  */
225
- protected abstract hydrate(ids: string[]): Promise<Map<string, unknown>>;
254
+ protected abstract hydrate(ids: string[], ctx?: ReadContext): Promise<Map<string, unknown>>;
226
255
 
227
256
  /** Provider payload → canonical record. Return `null` to drop a record. */
228
257
  protected abstract toCanonical(raw: unknown): T | null;
@@ -256,11 +285,14 @@ export abstract class IncrementalReadBase<T, F = unknown, M = Record<string, unk
256
285
  * adapter cannot hydrate-then-discard. A hydrate miss (deleted mid-run) is
257
286
  * skipped, never fabricated.
258
287
  */
259
- async *read(req: ReadRequest<F>): AsyncIterable<SourcedRecord<T>> {
260
- for await (const refPage of this.enumerate(req.mode, req.filter, req.pageSize)) {
288
+ async *read(req: ReadRequest<F>, ctx?: ReadContext): AsyncIterable<SourcedRecord<T>> {
289
+ for await (const refPage of this.enumerate(req.mode, req.filter, req.pageSize, ctx)) {
261
290
  const kept = refPage.filter((ref) => this.matchesRef(ref, req.filter));
262
291
  if (kept.length === 0) continue;
263
- const raws = await this.hydrate(kept.map((ref) => ref.externalId));
292
+ const raws = await this.hydrate(
293
+ kept.map((ref) => ref.externalId),
294
+ ctx,
295
+ );
264
296
  for (const ref of kept) {
265
297
  const raw = raws.get(ref.externalId);
266
298
  if (raw === undefined || raw === null) continue; // deleted mid-run → skip
@@ -277,8 +309,8 @@ export abstract class IncrementalReadBase<T, F = unknown, M = Record<string, unk
277
309
  * `toCanonical ∘ hydrate([id])`. Reuses the adapter's batched fetch + miss
278
310
  * tolerance; returns `null` for a missing or undecodable record.
279
311
  */
280
- async get(id: string): Promise<T | null> {
281
- const raws = await this.hydrate([id]);
312
+ async get(id: string, ctx?: ReadContext): Promise<T | null> {
313
+ const raws = await this.hydrate([id], ctx);
282
314
  const raw = raws.get(id);
283
315
  if (raw === undefined || raw === null) return null;
284
316
  return this.toCanonical(raw);
@@ -308,7 +340,9 @@ export abstract class IncrementalReadBase<T, F = unknown, M = Record<string, unk
308
340
  ? { kind: 'full' }
309
341
  : { kind: 'delta', cursor };
310
342
  const filter = this.filterFor(subscription);
311
- const stream = this.read({ mode, filter });
343
+ // R5: thread the run's subscription into the read body so `enumerate`/`hydrate`
344
+ // can resolve per-connection credentials (and raw-landing keys) from it.
345
+ const stream = this.read({ mode, filter }, { subscription });
312
346
 
313
347
  if (this.cursorDivisible) {
314
348
  for await (const sourced of stream) {
@@ -101,6 +101,7 @@ export {
101
101
  mapConcurrent,
102
102
  type IncrementalRead,
103
103
  type RandomRead,
104
+ type ReadContext,
104
105
  type ReadMode,
105
106
  type ReadRequest,
106
107
  type Ref,