@reprova/sdk 0.5.0 → 0.6.0

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.
Files changed (145) hide show
  1. package/README.md +75 -0
  2. package/dist/cjs/context.js +28 -0
  3. package/dist/cjs/distributionBaselinePoller.js +87 -0
  4. package/dist/cjs/distributionDrift.js +170 -0
  5. package/dist/cjs/drizzle.js +204 -0
  6. package/dist/cjs/frameworkAdapter.js +2 -0
  7. package/dist/cjs/httpFetch.js +59 -0
  8. package/dist/cjs/idempotency.js +132 -0
  9. package/dist/cjs/idempotencySettingsPoller.js +71 -0
  10. package/dist/cjs/index.js +96 -0
  11. package/dist/cjs/invariantInference.js +90 -0
  12. package/dist/cjs/invariantRules.js +142 -0
  13. package/dist/cjs/knex.js +86 -0
  14. package/dist/cjs/kysely.js +175 -0
  15. package/dist/cjs/legacyPrisma.js +130 -0
  16. package/dist/cjs/mikroorm.js +74 -0
  17. package/dist/cjs/mongoSchemaReporter.js +85 -0
  18. package/dist/cjs/mongoose.js +335 -0
  19. package/dist/cjs/nPlusOne.js +0 -0
  20. package/dist/cjs/nest.js +136 -0
  21. package/dist/cjs/nextjs.js +180 -0
  22. package/dist/cjs/package.json +1 -0
  23. package/dist/cjs/prisma.js +363 -0
  24. package/dist/cjs/proto.gen.js +4 -0
  25. package/dist/cjs/registry.js +100 -0
  26. package/dist/cjs/retryStorm.js +70 -0
  27. package/dist/cjs/rulesPoller.js +77 -0
  28. package/dist/cjs/sdk.js +1231 -0
  29. package/dist/cjs/sequelize.js +108 -0
  30. package/dist/cjs/shape.js +45 -0
  31. package/dist/cjs/stack.js +51 -0
  32. package/dist/cjs/transport.js +71 -0
  33. package/dist/cjs/typeorm.js +200 -0
  34. package/dist/context.d.ts +18 -0
  35. package/dist/context.d.ts.map +1 -1
  36. package/dist/context.js.map +1 -1
  37. package/dist/distributionBaselinePoller.d.ts +36 -0
  38. package/dist/distributionBaselinePoller.d.ts.map +1 -0
  39. package/dist/distributionBaselinePoller.js +84 -0
  40. package/dist/distributionBaselinePoller.js.map +1 -0
  41. package/dist/distributionDrift.d.ts +41 -0
  42. package/dist/distributionDrift.d.ts.map +1 -0
  43. package/dist/distributionDrift.js +161 -0
  44. package/dist/distributionDrift.js.map +1 -0
  45. package/dist/drizzle.d.ts.map +1 -1
  46. package/dist/drizzle.js +10 -4
  47. package/dist/drizzle.js.map +1 -1
  48. package/dist/frameworkAdapter.d.ts +5 -0
  49. package/dist/frameworkAdapter.d.ts.map +1 -0
  50. package/dist/frameworkAdapter.js +2 -0
  51. package/dist/frameworkAdapter.js.map +1 -0
  52. package/dist/httpFetch.d.ts +13 -0
  53. package/dist/httpFetch.d.ts.map +1 -0
  54. package/dist/httpFetch.js +54 -0
  55. package/dist/httpFetch.js.map +1 -0
  56. package/dist/idempotency.d.ts +26 -0
  57. package/dist/idempotency.d.ts.map +1 -0
  58. package/dist/idempotency.js +124 -0
  59. package/dist/idempotency.js.map +1 -0
  60. package/dist/idempotencySettingsPoller.d.ts +23 -0
  61. package/dist/idempotencySettingsPoller.d.ts.map +1 -0
  62. package/dist/idempotencySettingsPoller.js +68 -0
  63. package/dist/idempotencySettingsPoller.js.map +1 -0
  64. package/dist/index.d.ts +30 -5
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +17 -3
  67. package/dist/index.js.map +1 -1
  68. package/dist/invariantInference.d.ts +28 -0
  69. package/dist/invariantInference.d.ts.map +1 -0
  70. package/dist/invariantInference.js +87 -0
  71. package/dist/invariantInference.js.map +1 -0
  72. package/dist/invariantRules.d.ts +16 -0
  73. package/dist/invariantRules.d.ts.map +1 -0
  74. package/dist/invariantRules.js +104 -0
  75. package/dist/invariantRules.js.map +1 -0
  76. package/dist/knex.d.ts.map +1 -1
  77. package/dist/knex.js +1 -0
  78. package/dist/knex.js.map +1 -1
  79. package/dist/kysely.d.ts.map +1 -1
  80. package/dist/kysely.js +2 -1
  81. package/dist/kysely.js.map +1 -1
  82. package/dist/legacyPrisma.d.ts +6 -0
  83. package/dist/legacyPrisma.d.ts.map +1 -0
  84. package/dist/legacyPrisma.js +128 -0
  85. package/dist/legacyPrisma.js.map +1 -0
  86. package/dist/mikroorm.d.ts +2 -0
  87. package/dist/mikroorm.d.ts.map +1 -0
  88. package/dist/mikroorm.js +72 -0
  89. package/dist/mikroorm.js.map +1 -0
  90. package/dist/mongoSchemaReporter.d.ts +23 -0
  91. package/dist/mongoSchemaReporter.d.ts.map +1 -0
  92. package/dist/mongoSchemaReporter.js +82 -0
  93. package/dist/mongoSchemaReporter.js.map +1 -0
  94. package/dist/mongoose.d.ts +48 -0
  95. package/dist/mongoose.d.ts.map +1 -0
  96. package/dist/mongoose.js +331 -0
  97. package/dist/mongoose.js.map +1 -0
  98. package/dist/nPlusOne.d.ts +52 -0
  99. package/dist/nPlusOne.d.ts.map +1 -0
  100. package/dist/nPlusOne.js +0 -0
  101. package/dist/nPlusOne.js.map +1 -0
  102. package/dist/nest.d.ts +13 -0
  103. package/dist/nest.d.ts.map +1 -1
  104. package/dist/nest.js +39 -0
  105. package/dist/nest.js.map +1 -1
  106. package/dist/nextjs.d.ts +27 -0
  107. package/dist/nextjs.d.ts.map +1 -0
  108. package/dist/nextjs.js +178 -0
  109. package/dist/nextjs.js.map +1 -0
  110. package/dist/prisma.d.ts +21 -5
  111. package/dist/prisma.d.ts.map +1 -1
  112. package/dist/prisma.js +247 -22
  113. package/dist/prisma.js.map +1 -1
  114. package/dist/proto.gen.d.ts +10 -2
  115. package/dist/proto.gen.d.ts.map +1 -1
  116. package/dist/registry.d.ts +1 -0
  117. package/dist/registry.d.ts.map +1 -1
  118. package/dist/registry.js +24 -0
  119. package/dist/registry.js.map +1 -1
  120. package/dist/retryStorm.d.ts +42 -0
  121. package/dist/retryStorm.d.ts.map +1 -0
  122. package/dist/retryStorm.js +65 -0
  123. package/dist/retryStorm.js.map +1 -0
  124. package/dist/rulesPoller.d.ts +26 -0
  125. package/dist/rulesPoller.d.ts.map +1 -0
  126. package/dist/rulesPoller.js +74 -0
  127. package/dist/rulesPoller.js.map +1 -0
  128. package/dist/sdk.d.ts +48 -7
  129. package/dist/sdk.d.ts.map +1 -1
  130. package/dist/sdk.js +619 -47
  131. package/dist/sdk.js.map +1 -1
  132. package/dist/sequelize.d.ts.map +1 -1
  133. package/dist/sequelize.js +4 -3
  134. package/dist/sequelize.js.map +1 -1
  135. package/dist/stack.d.ts.map +1 -1
  136. package/dist/stack.js +6 -3
  137. package/dist/stack.js.map +1 -1
  138. package/dist/transport.d.ts.map +1 -1
  139. package/dist/transport.js +2 -1
  140. package/dist/transport.js.map +1 -1
  141. package/dist/typeorm.d.ts +1 -0
  142. package/dist/typeorm.d.ts.map +1 -1
  143. package/dist/typeorm.js +133 -1
  144. package/dist/typeorm.js.map +1 -1
  145. package/package.json +14 -5
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createKyselyPlugin = createKyselyPlugin;
4
+ const context_js_1 = require("./context.js");
5
+ const OP_BY_KIND = {
6
+ SelectQueryNode: 'select',
7
+ InsertQueryNode: 'insert',
8
+ UpdateQueryNode: 'update',
9
+ DeleteQueryNode: 'delete',
10
+ MergeQueryNode: 'merge',
11
+ };
12
+ // TableNode → its identifier name; unwraps an AliasNode (`from('t as x')`).
13
+ function tableName(node) {
14
+ const n = node;
15
+ if (!n || typeof n !== 'object')
16
+ return undefined;
17
+ if (n.kind === 'AliasNode')
18
+ return tableName(n.node);
19
+ if (n.kind === 'TableNode') {
20
+ const table = n.table;
21
+ return table?.identifier?.name;
22
+ }
23
+ return undefined;
24
+ }
25
+ // The operation's principal table, read from the op-specific field.
26
+ function primaryTable(node) {
27
+ switch (node.kind) {
28
+ case 'SelectQueryNode':
29
+ case 'DeleteQueryNode': {
30
+ const from = node.from;
31
+ return tableName(from?.froms?.[0]);
32
+ }
33
+ case 'InsertQueryNode':
34
+ return tableName(node.into);
35
+ case 'UpdateQueryNode':
36
+ return tableName(node.table);
37
+ default:
38
+ return undefined;
39
+ }
40
+ }
41
+ // Every table referenced anywhere in the query (joins, subqueries, CTEs, …),
42
+ // in first-seen order. A bounded deep-walk — instrumentation must not blow the
43
+ // stack or hang on a pathological tree.
44
+ function collectTables(root) {
45
+ const found = [];
46
+ const seen = new Set();
47
+ const stack = [root];
48
+ let budget = 5000;
49
+ while (stack.length && budget-- > 0) {
50
+ const cur = stack.pop();
51
+ if (!cur || typeof cur !== 'object')
52
+ continue;
53
+ const name = tableName(cur);
54
+ if (name && !seen.has(name)) {
55
+ seen.add(name);
56
+ found.push(name);
57
+ }
58
+ for (const v of Object.values(cur)) {
59
+ if (v && typeof v === 'object')
60
+ stack.push(v);
61
+ }
62
+ }
63
+ return found;
64
+ }
65
+ // Column NAMES referenced inside a WhereNode subtree — never values.
66
+ function whereColumns(whereNode) {
67
+ const cols = new Set();
68
+ const stack = [whereNode];
69
+ let budget = 5000;
70
+ while (stack.length && budget-- > 0) {
71
+ const cur = stack.pop();
72
+ if (!cur || typeof cur !== 'object')
73
+ continue;
74
+ const n = cur;
75
+ if (n.kind === 'ColumnNode') {
76
+ const col = n.column;
77
+ if (col?.name)
78
+ cols.add(col.name);
79
+ }
80
+ for (const v of Object.values(n)) {
81
+ if (v && typeof v === 'object')
82
+ stack.push(v);
83
+ }
84
+ }
85
+ return [...cols].sort();
86
+ }
87
+ function extractPks(rows, pkCols) {
88
+ if (pkCols.length === 0)
89
+ return [];
90
+ // A set: a join can return the same primary-table row many times, but the
91
+ // footprint records which DISTINCT rows were touched.
92
+ const seen = new Set();
93
+ const pks = [];
94
+ for (const row of rows) {
95
+ const vals = pkCols.map((c) => row[c]);
96
+ if (vals.some((v) => v === undefined))
97
+ continue; // column not selected — can't form a PK
98
+ const pk = vals.map((v) => String(v)).join(':');
99
+ if (!seen.has(pk)) {
100
+ seen.add(pk);
101
+ pks.push(pk);
102
+ }
103
+ }
104
+ return pks;
105
+ }
106
+ // Creates the Reprova Kysely footprint plugin.
107
+ // Usage: new Kysely({ dialect, plugins: [createKyselyPlugin()] })
108
+ function createKyselyPlugin(opts = {}) {
109
+ const primaryKey = opts.primaryKey ?? (() => ['id']);
110
+ const captureWhereShape = opts.captureWhereShape !== false;
111
+ // Keyed by the queryId OBJECT shared across both hooks; a compiled-but-never-
112
+ // executed query is GC'd without leaking.
113
+ const pending = new WeakMap();
114
+ return {
115
+ transformQuery(args) {
116
+ try {
117
+ const node = args.node;
118
+ const op = OP_BY_KIND[node.kind];
119
+ if (op) {
120
+ const whereShape = captureWhereShape && node.where
121
+ ? whereColumns(node.where).join(',') || undefined
122
+ : undefined;
123
+ pending.set(args.queryId, {
124
+ op,
125
+ primary: primaryTable(node),
126
+ tables: collectTables(node),
127
+ whereShape,
128
+ });
129
+ }
130
+ }
131
+ catch {
132
+ // A footprint failure must never alter the query — fall through.
133
+ }
134
+ return args.node;
135
+ },
136
+ async transformResult(args) {
137
+ try {
138
+ const meta = pending.get(args.queryId);
139
+ if (meta) {
140
+ pending.delete(args.queryId);
141
+ const ctx = context_js_1.contextStorage.getStore();
142
+ if (ctx) {
143
+ const result = args.result;
144
+ const rows = result.rows ?? [];
145
+ const affected = result.numAffectedRows != null ? Number(result.numAffectedRows) : undefined;
146
+ const count = rows.length || affected || 0;
147
+ const primary = meta.primary ?? '$unknown';
148
+ const entry = {
149
+ model: primary,
150
+ op: meta.op,
151
+ pks: extractPks(rows, primaryKey(primary)),
152
+ where_shape: meta.whereShape,
153
+ count,
154
+ note: count === 0 ? 'returned 0 rows' : undefined,
155
+ resolved: true,
156
+ };
157
+ ctx.footprint.push(entry);
158
+ // Joined / subquery tables: recorded so the extractor knows they
159
+ // were touched, but without PKs (result columns can't be reliably
160
+ // attributed across a join).
161
+ for (const t of meta.tables) {
162
+ if (t === primary)
163
+ continue;
164
+ ctx.footprint.push({ model: t, op: meta.op, pks: [], count: 0, note: 'referenced (join/subquery)', resolved: true });
165
+ }
166
+ }
167
+ }
168
+ }
169
+ catch {
170
+ // Never break the result pipeline over a footprint error.
171
+ }
172
+ return args.result;
173
+ },
174
+ };
175
+ }
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.instrumentLegacyPrismaClient = instrumentLegacyPrismaClient;
4
+ const context_js_1 = require("./context.js");
5
+ const prisma_js_1 = require("./prisma.js");
6
+ // Older Prisma clients have no query-lifecycle hook at all — confirmed live
7
+ // against a real @prisma/client@2.0.0-beta.4 (2020): $extends (introduced
8
+ // 4.16, 2023), $use (Prisma's earlier middleware API, introduced ~2.something
9
+ // and itself later deprecated in favor of $extends), and even $queryRaw are
10
+ // all `undefined` on that client. createPrismaExtension's $allOperations has
11
+ // nothing to attach to on a client like this.
12
+ //
13
+ // What IS there, on every Prisma client generator back to the very first
14
+ // one: a plain per-model delegate object (client.article, client.user, ...)
15
+ // whose CRUD methods (findMany, create, update, ...) are ordinary
16
+ // own-property functions — directly reassignable, the same monkey-patch
17
+ // shape this SDK already uses for TypeORM's QueryRunner.prototype.query and
18
+ // Mongoose's already-compiled models (instrumentAlreadyCompiledModels). This
19
+ // wraps each one directly instead of relying on any official extension
20
+ // point, so footprint capture and every detector built on recordPrismaOperation
21
+ // work identically regardless of how old the installed client is.
22
+ //
23
+ // Also captures DB-rejected writes the same way typeorm.ts does for a
24
+ // TypeORM app: create/update/upsert/delete/updateMany/deleteMany record an
25
+ // `<op>_failed` footprint entry on rejection — column NAMES only (from the
26
+ // call's own `data`/`where` argument keys), never the bound values, per the
27
+ // metadata-only rule. A read failing has no "what would have been written"
28
+ // story, so only write ops get this treatment, matching typeorm.ts's scope.
29
+ const READ_OPS = new Set(['findOne', 'findUnique', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow', 'findMany', 'count', 'aggregate', 'groupBy']);
30
+ const WRITE_OPS = new Set(['create', 'createMany', 'update', 'updateMany', 'upsert', 'delete', 'deleteMany']);
31
+ // `findOne` is Prisma's own deprecated alias for `findUnique` (renamed
32
+ // well before $extends existed, still present on clients this old) —
33
+ // normalized before recording so the footprint's `op` field and
34
+ // operationMatchesHook's after_read gating (invariantRules.ts's READ_OPS,
35
+ // which only lists modern names) both see the same vocabulary regardless
36
+ // of which Prisma version actually captured it.
37
+ function normalizeOperation(method) {
38
+ return method === 'findOne' ? 'findUnique' : method;
39
+ }
40
+ // Reassigns each detected model delegate's own CRUD methods exactly once —
41
+ // re-instrumenting an already-patched delegate (e.g. instrumentPrisma called
42
+ // twice) would double-record every footprint entry.
43
+ const patchedDelegates = new WeakSet();
44
+ function instrumentLegacyPrismaClient(client, opts = {}) {
45
+ try {
46
+ const dmmf = opts.dmmf ?? client.dmmf;
47
+ const hooks = {
48
+ dmmf,
49
+ getRules: opts.getRules,
50
+ inferenceReporter: opts.inferenceReporter,
51
+ writeCountDriftReporter: opts.writeCountDriftReporter,
52
+ writeCountBaselinePoller: opts.writeCountBaselinePoller,
53
+ };
54
+ const modelNameFor = buildModelNameResolver(dmmf);
55
+ for (const [key, delegate] of Object.entries(client)) {
56
+ if (!isModelDelegate(delegate))
57
+ continue;
58
+ if (patchedDelegates.has(delegate))
59
+ continue;
60
+ patchedDelegates.add(delegate);
61
+ const modelName = modelNameFor(key);
62
+ for (const method of Object.keys(delegate)) {
63
+ if (READ_OPS.has(method)) {
64
+ patchReadMethod(delegate, method, modelName, hooks);
65
+ }
66
+ else if (WRITE_OPS.has(method)) {
67
+ patchWriteMethod(delegate, method, modelName, hooks);
68
+ }
69
+ }
70
+ }
71
+ }
72
+ catch {
73
+ /* never throw from install */
74
+ }
75
+ }
76
+ function isModelDelegate(value) {
77
+ if (!value || typeof value !== 'object')
78
+ return false;
79
+ const d = value;
80
+ return typeof d.findMany === 'function' || typeof d.create === 'function';
81
+ }
82
+ // Prisma's own convention: a model named `Article` gets a delegate property
83
+ // named `article` (first letter lowercased) — true on every version this
84
+ // adapter targets. Falls back to that convention when dmmf isn't available
85
+ // at all; uses the real dmmf model list when it is, so an unconventional
86
+ // name still resolves correctly.
87
+ function buildModelNameResolver(dmmf) {
88
+ if (!dmmf) {
89
+ return (key) => key.charAt(0).toUpperCase() + key.slice(1);
90
+ }
91
+ const byLowerFirst = new Map();
92
+ for (const model of dmmf.datamodel.models) {
93
+ byLowerFirst.set(model.name.charAt(0).toLowerCase() + model.name.slice(1), model.name);
94
+ }
95
+ return (key) => byLowerFirst.get(key) ?? key.charAt(0).toUpperCase() + key.slice(1);
96
+ }
97
+ function patchReadMethod(delegate, method, modelName, hooks) {
98
+ const original = delegate[method];
99
+ const operation = normalizeOperation(method);
100
+ delegate[method] = async function (...args) {
101
+ const result = await original.apply(this, args);
102
+ const ctx = context_js_1.contextStorage.getStore();
103
+ if (ctx) {
104
+ const callArgs = args[0] ?? {};
105
+ await (0, prisma_js_1.recordPrismaOperation)(ctx, modelName, operation, callArgs, result, hooks);
106
+ }
107
+ return result;
108
+ };
109
+ }
110
+ function patchWriteMethod(delegate, method, modelName, hooks) {
111
+ const original = delegate[method];
112
+ delegate[method] = async function (...args) {
113
+ const callArgs = args[0] ?? {};
114
+ let result;
115
+ try {
116
+ result = await original.apply(this, args);
117
+ }
118
+ catch (err) {
119
+ const ctx = context_js_1.contextStorage.getStore();
120
+ if (ctx)
121
+ (0, prisma_js_1.recordFailedWrite)(ctx, modelName, method, callArgs);
122
+ throw err;
123
+ }
124
+ const ctx = context_js_1.contextStorage.getStore();
125
+ if (ctx) {
126
+ await (0, prisma_js_1.recordPrismaOperation)(ctx, modelName, method, callArgs, result, hooks);
127
+ }
128
+ return result;
129
+ };
130
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installMikroOrmSubscriber = installMikroOrmSubscriber;
4
+ const context_js_1 = require("./context.js");
5
+ function pkOf(entity, meta) {
6
+ const keys = meta?.primaryKeys ?? [];
7
+ if (!entity || keys.length === 0)
8
+ return null;
9
+ const vals = keys.map((k) => entity[k]);
10
+ if (vals.some((v) => v === undefined || v === null))
11
+ return null;
12
+ return vals.map((v) => String(v)).join(':');
13
+ }
14
+ // Find-or-create the (table, op) entry for this request and fold in the PK.
15
+ function recordPk(table, op, pk) {
16
+ const ctx = context_js_1.contextStorage.getStore();
17
+ if (!ctx || !table)
18
+ return;
19
+ let entry = ctx.footprint.find((f) => f.model === table && f.op === op);
20
+ if (!entry) {
21
+ entry = { model: table, op, pks: [], count: 0 };
22
+ ctx.footprint.push(entry);
23
+ }
24
+ if (pk !== null && !entry.pks.includes(pk))
25
+ entry.pks.push(pk);
26
+ entry.count = entry.pks.length;
27
+ }
28
+ // Accepts either a MikroORM instance (orm.em.getEventManager()) or an
29
+ // EntityManager directly (em.getEventManager()) — Reprova has no way to
30
+ // know which one a given host app kept a reference to.
31
+ function getEventManager(orm) {
32
+ const o = orm;
33
+ if (typeof o.getEventManager === 'function')
34
+ return o.getEventManager();
35
+ if (o.em && typeof o.em.getEventManager === 'function')
36
+ return o.em.getEventManager();
37
+ return undefined;
38
+ }
39
+ // Installs a footprint subscriber on an initialized MikroORM instance (or
40
+ // EntityManager). Call AFTER MikroORM.init() — the EventManager only
41
+ // exists once the ORM is initialized. Registers a plain object, so the
42
+ // host needs no @Subscriber()-decorated class.
43
+ function installMikroOrmSubscriber(orm) {
44
+ const subscriber = {
45
+ onLoad(args) {
46
+ try {
47
+ recordPk(args?.meta?.className, 'select', pkOf(args?.entity, args?.meta));
48
+ }
49
+ catch { /* never break */ }
50
+ },
51
+ afterCreate(args) {
52
+ try {
53
+ recordPk(args?.meta?.className, 'insert', pkOf(args?.entity, args?.meta));
54
+ }
55
+ catch { /* ignore */ }
56
+ },
57
+ afterUpdate(args) {
58
+ try {
59
+ recordPk(args?.meta?.className, 'update', pkOf(args?.entity, args?.meta));
60
+ }
61
+ catch { /* ignore */ }
62
+ },
63
+ afterDelete(args) {
64
+ try {
65
+ recordPk(args?.meta?.className, 'delete', pkOf(args?.entity, args?.meta));
66
+ }
67
+ catch { /* ignore */ }
68
+ },
69
+ };
70
+ try {
71
+ getEventManager(orm)?.registerSubscriber(subscriber);
72
+ }
73
+ catch { /* never throw from install */ }
74
+ }
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MongoSchemaReporter = void 0;
4
+ const mongoose_js_1 = require("./mongoose.js");
5
+ const httpFetch_js_1 = require("./httpFetch.js");
6
+ // Periodically re-scans a live Mongoose instance's model registry and
7
+ // reports the discovered model->collection map plus every ref: edge found,
8
+ // so the control plane can hand the agent a reference-edge map at
9
+ // plan-build time (MongoDB has no FK catalog for the agent to introspect
10
+ // itself — see mongo_schema.go's handler doc comment). Unlike
11
+ // DistributionDriftReporter (accumulates external .record() deltas since
12
+ // its last flush), this reporter is self-driven: it re-derives the current
13
+ // schema shape itself on each tick and only POSTs when that shape actually
14
+ // changed, since schema shape is near-static and re-sending an unchanged
15
+ // report every tick for the life of a long-running process would be pure
16
+ // waste.
17
+ const WARN_INTERVAL_MS = 60_000;
18
+ const DEFAULT_INTERVAL_MS = 60_000;
19
+ class MongoSchemaReporter {
20
+ timer = null;
21
+ lastSentJSON = null;
22
+ lastWarnAt = 0;
23
+ endpoint;
24
+ apiKey;
25
+ intervalMs;
26
+ mongooseInstance;
27
+ constructor(opts) {
28
+ this.endpoint = opts.endpoint;
29
+ this.apiKey = opts.apiKey;
30
+ this.intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
31
+ this.mongooseInstance = opts.mongooseInstance;
32
+ }
33
+ start() {
34
+ void this.tick(); // report the current shape immediately, don't wait a full interval for the first report
35
+ this.timer = setInterval(() => void this.tick(), this.intervalMs);
36
+ if (this.timer.unref)
37
+ this.timer.unref();
38
+ }
39
+ stop() {
40
+ if (this.timer) {
41
+ clearInterval(this.timer);
42
+ this.timer = null;
43
+ }
44
+ }
45
+ // Exposed for tests and for an explicit "report now" call after a
46
+ // dynamic model registration; start()/the timer call this on their own.
47
+ async tick() {
48
+ let discovered;
49
+ try {
50
+ discovered = (0, mongoose_js_1.discoverMongooseSchema)(this.mongooseInstance);
51
+ }
52
+ catch {
53
+ return; // never let introspection failure break the host app
54
+ }
55
+ if (discovered.models.length === 0 && discovered.edges.length === 0)
56
+ return;
57
+ const json = JSON.stringify(discovered);
58
+ if (json === this.lastSentJSON)
59
+ return; // schema shape unchanged since last report
60
+ try {
61
+ const res = await (0, httpFetch_js_1.httpFetch)(this.endpoint, {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json', 'X-Reprova-Key': this.apiKey },
64
+ body: JSON.stringify({ models: discovered.models, edges: discovered.edges }),
65
+ });
66
+ if (res.ok) {
67
+ this.lastSentJSON = json;
68
+ }
69
+ else {
70
+ this.warnOnce(`reprova: mongo schema report returned ${res.status}`);
71
+ }
72
+ }
73
+ catch (err) {
74
+ this.warnOnce(`reprova: mongo schema report error: ${err.message}`);
75
+ }
76
+ }
77
+ warnOnce(msg) {
78
+ const now = Date.now();
79
+ if (now - this.lastWarnAt > WARN_INTERVAL_MS) {
80
+ this.lastWarnAt = now;
81
+ console.warn(msg);
82
+ }
83
+ }
84
+ }
85
+ exports.MongoSchemaReporter = MongoSchemaReporter;