@deepexi/datasense-connector 3.0.1 → 3.1.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 (185) hide show
  1. package/README.md +158 -139
  2. package/package.json +19 -28
  3. package/scripts/build.ts +188 -0
  4. package/src/define.ts +267 -0
  5. package/src/descriptor.ts +116 -0
  6. package/src/errors.ts +105 -0
  7. package/src/index.ts +108 -0
  8. package/src/internal/location.ts +23 -0
  9. package/src/runtime/guest/bridge.ts +238 -0
  10. package/src/runtime/guest/cache.ts +43 -0
  11. package/src/runtime/guest/check.ts +17 -0
  12. package/src/runtime/guest/crypto.ts +166 -0
  13. package/src/runtime/guest/ctx.ts +157 -0
  14. package/src/runtime/guest/db.ts +59 -0
  15. package/src/runtime/guest/failure.ts +69 -0
  16. package/src/runtime/guest/http.ts +194 -0
  17. package/src/runtime/guest/index.ts +102 -0
  18. package/{runtime/guest/limits.d.ts → src/runtime/guest/limits.ts} +12 -12
  19. package/src/runtime/guest/loader.ts +210 -0
  20. package/src/runtime/guest/log.ts +47 -0
  21. package/src/runtime/guest/map.ts +45 -0
  22. package/src/runtime/guest/operation.ts +193 -0
  23. package/src/runtime/guest/paginate.ts +169 -0
  24. package/src/runtime/guest/pull.ts +177 -0
  25. package/src/runtime/guest/push.ts +92 -0
  26. package/src/runtime/guest/signal.ts +14 -0
  27. package/src/runtime/guest/suggest.ts +51 -0
  28. package/src/runtime/guest/time.ts +137 -0
  29. package/src/runtime/guest-runtime.ts +75 -0
  30. package/src/runtime/index.ts +53 -0
  31. package/src/runtime/protocol.ts +557 -0
  32. package/src/runtime/session.ts +216 -0
  33. package/src/testing/child.ts +21 -0
  34. package/src/testing/compliance.ts +343 -0
  35. package/src/testing/engine.ts +445 -0
  36. package/src/testing/fixtures.ts +591 -0
  37. package/src/testing/gateway.ts +482 -0
  38. package/src/testing/import-guard.ts +221 -0
  39. package/src/testing/index.ts +43 -0
  40. package/src/testing/local-db.ts +226 -0
  41. package/src/testing/mock-external-system.ts +249 -0
  42. package/src/testing/pinned-http.ts +144 -0
  43. package/src/testing/recorder.ts +140 -0
  44. package/src/testing/redaction.ts +94 -0
  45. package/src/testing/replay.ts +437 -0
  46. package/src/testing/run-tests.ts +115 -0
  47. package/src/testing/schema.ts +70 -0
  48. package/src/testing/source-loader.ts +374 -0
  49. package/src/testing/types.ts +78 -0
  50. package/src/types.ts +329 -0
  51. package/src/validate.ts +267 -0
  52. package/test/build.test.ts +159 -0
  53. package/test/define.test-d.ts +182 -0
  54. package/test/fixtures/acceptance.ts +156 -0
  55. package/test/fixtures/connectors/http-pull/.ontos/descriptor.json +72 -0
  56. package/test/fixtures/connectors/http-pull/.ontos/types.ts +61 -0
  57. package/test/fixtures/connectors/http-pull/package.json +5 -0
  58. package/test/fixtures/connectors/http-pull/streams/items/index.ts +37 -0
  59. package/test/fixtures/connectors/http-pull/streams/items/map.ts +12 -0
  60. package/test/fixtures/connectors/http-pull/streams/items/tests/basic/case.json +15 -0
  61. package/test/fixtures/connectors/http-pull/streams/items/tests/basic/exchanges.ndjson +2 -0
  62. package/test/fixtures/connectors/pg-pull/.ontos/descriptor.json +151 -0
  63. package/test/fixtures/connectors/pg-pull/.ontos/types.ts +62 -0
  64. package/test/fixtures/connectors/pg-pull/lib/sql.ts +14 -0
  65. package/test/fixtures/connectors/pg-pull/package.json +5 -0
  66. package/test/fixtures/connectors/pg-pull/streams/customers/index.ts +30 -0
  67. package/test/fixtures/connectors/pg-pull/streams/customers/tests/customers-full/case.json +15 -0
  68. package/test/fixtures/connectors/pg-pull/streams/customers/tests/customers-full/exchanges.ndjson +3 -0
  69. package/test/fixtures/connectors/pg-pull/streams/orders/index.ts +32 -0
  70. package/test/fixtures/descriptor.ts +54 -0
  71. package/test/fixtures/kingdee-example.ts +82 -0
  72. package/test/fixtures/record-acceptance.ts +32 -0
  73. package/test/runtime/ctx.test.ts +434 -0
  74. package/test/runtime/helpers.ts +157 -0
  75. package/test/runtime/jobs.test.ts +444 -0
  76. package/test/runtime/operation.test.ts +175 -0
  77. package/test/runtime/session.test.ts +229 -0
  78. package/test/testing/acceptance-fixtures.test.ts +69 -0
  79. package/test/testing/compliance.test.ts +247 -0
  80. package/test/testing/engine.test.ts +283 -0
  81. package/test/testing/fixtures.test.ts +221 -0
  82. package/test/testing/gateway.test.ts +337 -0
  83. package/test/testing/import-guard.test.ts +45 -0
  84. package/test/testing/mock-external-system.test.ts +144 -0
  85. package/test/testing/operation-fixtures.test.ts +160 -0
  86. package/test/testing/recorder.test.ts +163 -0
  87. package/test/testing/redaction.test.ts +54 -0
  88. package/test/testing/replay.test.ts +207 -0
  89. package/test/testing/run-tests.test.ts +137 -0
  90. package/test/testing/source-loader.test.ts +220 -0
  91. package/test/validate-config.test.ts +104 -0
  92. package/tsconfig.build.json +23 -0
  93. package/tsconfig.json +19 -0
  94. package/cjs/index.cjs +0 -1015
  95. package/cjs/runtime.cjs +0 -752
  96. package/cjs/testing.cjs +0 -3591
  97. package/errors.d.ts +0 -17
  98. package/errors.js +0 -53
  99. package/guest.cjs +0 -2659
  100. package/index.d.ts +0 -13
  101. package/index.js +0 -7
  102. package/internal/location.d.ts +0 -6
  103. package/internal/location.js +0 -8
  104. package/presentation.d.ts +0 -38
  105. package/presentation.js +0 -268
  106. package/runtime/guest/bridge.d.ts +0 -53
  107. package/runtime/guest/bridge.js +0 -187
  108. package/runtime/guest/cache.d.ts +0 -3
  109. package/runtime/guest/cache.js +0 -42
  110. package/runtime/guest/crypto.d.ts +0 -13
  111. package/runtime/guest/crypto.js +0 -177
  112. package/runtime/guest/ctx.d.ts +0 -30
  113. package/runtime/guest/ctx.js +0 -124
  114. package/runtime/guest/db.d.ts +0 -6
  115. package/runtime/guest/db.js +0 -59
  116. package/runtime/guest/discover.d.ts +0 -29
  117. package/runtime/guest/discover.js +0 -51
  118. package/runtime/guest/failure.d.ts +0 -24
  119. package/runtime/guest/failure.js +0 -63
  120. package/runtime/guest/http.d.ts +0 -4
  121. package/runtime/guest/http.js +0 -204
  122. package/runtime/guest/index.d.ts +0 -12
  123. package/runtime/guest/index.js +0 -89
  124. package/runtime/guest/limits.js +0 -12
  125. package/runtime/guest/loader.d.ts +0 -15
  126. package/runtime/guest/loader.js +0 -42
  127. package/runtime/guest/log.d.ts +0 -10
  128. package/runtime/guest/log.js +0 -36
  129. package/runtime/guest/map.d.ts +0 -4
  130. package/runtime/guest/map.js +0 -45
  131. package/runtime/guest/paginate.d.ts +0 -6
  132. package/runtime/guest/paginate.js +0 -180
  133. package/runtime/guest/pull.d.ts +0 -12
  134. package/runtime/guest/pull.js +0 -150
  135. package/runtime/guest/push.d.ts +0 -18
  136. package/runtime/guest/push.js +0 -80
  137. package/runtime/guest/signal.d.ts +0 -3
  138. package/runtime/guest/signal.js +0 -12
  139. package/runtime/guest/time.d.ts +0 -5
  140. package/runtime/guest/time.js +0 -127
  141. package/runtime/guest-runtime.d.ts +0 -7
  142. package/runtime/guest-runtime.js +0 -51
  143. package/runtime/index.d.ts +0 -18
  144. package/runtime/index.js +0 -3
  145. package/runtime/protocol.d.ts +0 -276
  146. package/runtime/protocol.js +0 -409
  147. package/runtime/session.d.ts +0 -12
  148. package/runtime/session.js +0 -191
  149. package/t.d.ts +0 -18
  150. package/t.js +0 -26
  151. package/testing/child.cjs +0 -350
  152. package/testing/child.d.ts +0 -1
  153. package/testing/child.js +0 -12
  154. package/testing/compliance.d.ts +0 -41
  155. package/testing/compliance.js +0 -291
  156. package/testing/engine.d.ts +0 -66
  157. package/testing/engine.js +0 -339
  158. package/testing/fixtures.d.ts +0 -145
  159. package/testing/fixtures.js +0 -407
  160. package/testing/gateway.d.ts +0 -65
  161. package/testing/gateway.js +0 -402
  162. package/testing/import-guard.d.ts +0 -57
  163. package/testing/import-guard.js +0 -187
  164. package/testing/index.d.ts +0 -33
  165. package/testing/index.js +0 -10
  166. package/testing/local-db.d.ts +0 -27
  167. package/testing/local-db.js +0 -181
  168. package/testing/pinned-http.d.ts +0 -14
  169. package/testing/pinned-http.js +0 -125
  170. package/testing/recorder.d.ts +0 -15
  171. package/testing/recorder.js +0 -116
  172. package/testing/redaction.d.ts +0 -28
  173. package/testing/redaction.js +0 -67
  174. package/testing/replay.d.ts +0 -50
  175. package/testing/replay.js +0 -313
  176. package/testing/schema.d.ts +0 -13
  177. package/testing/schema.js +0 -65
  178. package/testing/source-loader.d.ts +0 -19
  179. package/testing/source-loader.js +0 -238
  180. package/testing/types.d.ts +0 -79
  181. package/testing/types.js +0 -18
  182. package/types.d.ts +0 -302
  183. package/types.js +0 -13
  184. package/validate.d.ts +0 -78
  185. package/validate.js +0 -597
@@ -0,0 +1,221 @@
1
+ /**
2
+ * 本地源码加载器的导入检查(不执行任何代码,只用调用方传入的 TypeScript 编译器 API 解析语法树)。
3
+ *
4
+ * 两层:
5
+ * 1. `findForbiddenSourceSyntax`:与 shared `server/connector-source-guard.ts` 的同名函数规则相同(SDK 不依赖 shared,
6
+ * 这里另写一份,`test/testing/import-guard.test.ts` 用同一批样例对照两份实现):
7
+ * - 任何 import 属性 / 断言(静态 `import`、`export … from` 的 `with` / `assert`,动态 `import(x, options)` 的第二参);
8
+ * - `macro:` 开头的说明符;
9
+ * - SDK 只认精确的 `@deepexi/datasense-connector`:子路径(含 `/testing`)与改名前的旧包名都拒绝;
10
+ * - TypeScript 解析出的语法错误。
11
+ * 2. `classifySpecifier`:转译后代码里每个 `require(…)` 的说明符(与服务端打包插件的白名单相同):只允许相对路径与
12
+ * `@deepexi/datasense-connector`;`node:*` 与 Node 内置模块、裸包名、绝对路径与 URL 说明符都拒绝;逃出源码根的相对路径
13
+ * 单独报 `PATH_ESCAPE`。
14
+ */
15
+ import type * as TS from 'typescript';
16
+
17
+ export type TypeScriptApi = typeof TS;
18
+
19
+ export interface SourceGuardDiagnostic {
20
+ /** 相对源码根目录的路径。 */
21
+ file: string;
22
+ /** 从 1 开始。 */
23
+ line: number;
24
+ /** 从 0 开始。 */
25
+ column: number;
26
+ message: string;
27
+ /** `syntax`:语法错误;`import`:禁用的导入写法。 */
28
+ kind: 'syntax' | 'import';
29
+ }
30
+
31
+ export const CONNECTOR_SDK_SPECIFIER = '@deepexi/datasense-connector';
32
+ /** 改名前的包名(拼接而成,让仓库里旧包名零出现的 grep 守卫保持干净)。 */
33
+ const RETIRED_SDK_SPECIFIER = ['@mu-ontos', 'connector-sdk'].join('/');
34
+ const MACRO_SPECIFIER = /^macro:/i;
35
+
36
+ /** Node 内置模块(不依赖 `node:module`:这份检查不该因运行时不同而结论不同)。 */
37
+ const NODE_BUILTINS: ReadonlySet<string> = new Set([
38
+ 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', 'constants', 'crypto', 'dgram', 'diagnostics_channel',
39
+ 'dns', 'domain', 'events', 'fs', 'http', 'http2', 'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks', 'process',
40
+ 'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'sys', 'timers', 'tls', 'trace_events', 'tty', 'url',
41
+ 'util', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib', 'test', 'sqlite', 'sea',
42
+ ]);
43
+
44
+ export function asTypeScript(value: unknown): TypeScriptApi {
45
+ const api = (value !== null && typeof value === 'object' && 'default' in value && typeof (value as { default?: unknown }).default === 'object'
46
+ ? (value as { default: unknown }).default
47
+ : value) as Partial<TypeScriptApi> | null;
48
+ if (!api || typeof api.createSourceFile !== 'function' || typeof api.transpileModule !== 'function') {
49
+ throw new TypeError('options.typescript must be the TypeScript compiler API (import ts from "typescript")');
50
+ }
51
+ return api as TypeScriptApi;
52
+ }
53
+
54
+ function scriptKindOf(ts: TypeScriptApi, filePath: string): TS.ScriptKind {
55
+ if (filePath.endsWith('.tsx')) return ts.ScriptKind.TSX;
56
+ if (filePath.endsWith('.jsx')) return ts.ScriptKind.JSX;
57
+ if (/\.[cm]?js$/.test(filePath)) return ts.ScriptKind.JS;
58
+ return ts.ScriptKind.TS;
59
+ }
60
+
61
+ function sdkSpecifierProblem(specifier: string): string | null {
62
+ if (specifier === RETIRED_SDK_SPECIFIER || specifier.startsWith(`${RETIRED_SDK_SPECIFIER}/`)) {
63
+ return `import '${specifier}' is not allowed; the SDK package is now '${CONNECTOR_SDK_SPECIFIER}'`;
64
+ }
65
+ if (specifier.startsWith(`${CONNECTOR_SDK_SPECIFIER}/`)) {
66
+ return `import '${specifier}' is not allowed; import '${CONNECTOR_SDK_SPECIFIER}' itself`;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ function literalSpecifier(ts: TypeScriptApi, node: TS.Node | undefined): string | null {
72
+ if (node && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))) return node.text;
73
+ return null;
74
+ }
75
+
76
+ /** 检查一个源码文件,返回全部违规(空数组表示通过)。规则与 shared 的 `findForbiddenSourceSyntax` 相同。 */
77
+ export function findForbiddenSourceSyntax(ts: TypeScriptApi, filePath: string, content: string): SourceGuardDiagnostic[] {
78
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindOf(ts, filePath));
79
+ const problems: SourceGuardDiagnostic[] = [];
80
+ const report = (position: number, message: string, kind: SourceGuardDiagnostic['kind']): void => {
81
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(position);
82
+ problems.push({ file: filePath, line: line + 1, column: character, message, kind });
83
+ };
84
+
85
+ // 公开类型里没有 `parseDiagnostics`,但运行时一直存在(shared 的守卫同样这样取)。
86
+ const parseDiagnostics = (sourceFile as unknown as { parseDiagnostics?: readonly TS.DiagnosticWithLocation[] }).parseDiagnostics ?? [];
87
+ for (const diagnostic of parseDiagnostics) {
88
+ report(diagnostic.start ?? 0, `syntax error: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, ' ')}`, 'syntax');
89
+ }
90
+
91
+ const checkSpecifier = (node: TS.Node | undefined): void => {
92
+ const specifier = literalSpecifier(ts, node);
93
+ if (!node || specifier === null) return;
94
+ if (MACRO_SPECIFIER.test(specifier)) {
95
+ report(node.getStart(sourceFile), `import '${specifier}' is not allowed; connectors cannot use Bun macros`, 'import');
96
+ return;
97
+ }
98
+ const sdkProblem = sdkSpecifierProblem(specifier);
99
+ if (sdkProblem !== null) report(node.getStart(sourceFile), sdkProblem, 'import');
100
+ };
101
+ const attributesMessage = (keyword: string): string =>
102
+ `import attributes ('${keyword} { … }') are not allowed; connectors cannot use Bun macros or other import attributes`;
103
+
104
+ const visit = (node: TS.Node): void => {
105
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
106
+ checkSpecifier(node.moduleSpecifier);
107
+ if (node.attributes) {
108
+ report(node.attributes.getStart(sourceFile), attributesMessage(node.attributes.token === ts.SyntaxKind.AssertKeyword ? 'assert' : 'with'), 'import');
109
+ }
110
+ } else if (ts.isImportEqualsDeclaration(node)) {
111
+ if (ts.isExternalModuleReference(node.moduleReference)) checkSpecifier(node.moduleReference.expression);
112
+ } else if (ts.isCallExpression(node)) {
113
+ const callee = node.expression;
114
+ if (callee.kind === ts.SyntaxKind.ImportKeyword) {
115
+ checkSpecifier(node.arguments[0]);
116
+ const options = node.arguments[1];
117
+ if (options) report(options.getStart(sourceFile), attributesMessage('with'), 'import');
118
+ } else if (ts.isIdentifier(callee) && callee.text === 'require') {
119
+ checkSpecifier(node.arguments[0]);
120
+ }
121
+ }
122
+ ts.forEachChild(node, visit);
123
+ };
124
+ visit(sourceFile);
125
+ return problems;
126
+ }
127
+
128
+ /** 转译后 CommonJS 代码里的 `require(…)`:`specifier` 为 null 表示参数不是字面量(运行时才知道导入什么)。 */
129
+ export function collectRequires(ts: TypeScriptApi, filePath: string, output: string): Array<{ specifier: string | null; line: number }> {
130
+ const sourceFile = ts.createSourceFile(`${filePath}.js`, output, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
131
+ const found: Array<{ specifier: string | null; line: number }> = [];
132
+ const visit = (node: TS.Node): void => {
133
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'require') {
134
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
135
+ found.push({ specifier: literalSpecifier(ts, node.arguments[0]), line: line + 1 });
136
+ }
137
+ ts.forEachChild(node, visit);
138
+ };
139
+ visit(sourceFile);
140
+ return found;
141
+ }
142
+
143
+ /**
144
+ * 源码里写出的值导入说明符(`import` / `export … from` / `import()` / `require()` 的字面量;`import type` 与 `export type` 不算)。
145
+ * 转译会删掉没用到的导入,只看转译结果会放过 `import fs from 'node:fs'` 这类写了没用的导入,所以源码这一层也要归类。
146
+ */
147
+ export function collectSourceSpecifiers(ts: TypeScriptApi, filePath: string, content: string): Array<{ specifier: string; line: number }> {
148
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindOf(ts, filePath));
149
+ const found: Array<{ specifier: string; line: number }> = [];
150
+ const add = (node: TS.Node | undefined): void => {
151
+ const specifier = literalSpecifier(ts, node);
152
+ if (!node || specifier === null) return;
153
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
154
+ found.push({ specifier, line: line + 1 });
155
+ };
156
+ const visit = (node: TS.Node): void => {
157
+ if (ts.isImportDeclaration(node)) {
158
+ if (!node.importClause?.isTypeOnly) add(node.moduleSpecifier);
159
+ } else if (ts.isExportDeclaration(node)) {
160
+ if (!node.isTypeOnly) add(node.moduleSpecifier);
161
+ } else if (ts.isImportEqualsDeclaration(node)) {
162
+ if (!node.isTypeOnly && ts.isExternalModuleReference(node.moduleReference)) add(node.moduleReference.expression);
163
+ } else if (ts.isCallExpression(node)) {
164
+ const callee = node.expression;
165
+ if (callee.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(callee) && callee.text === 'require')) add(node.arguments[0]);
166
+ }
167
+ ts.forEachChild(node, visit);
168
+ };
169
+ visit(sourceFile);
170
+ return found;
171
+ }
172
+
173
+ export type SpecifierClass =
174
+ | { kind: 'sdk' }
175
+ | { kind: 'relative'; target: string }
176
+ | { kind: 'escape' }
177
+ | { kind: 'forbidden'; message: string };
178
+
179
+ function packageNameOf(specifier: string): string {
180
+ const parts = specifier.split('/');
181
+ if (specifier.startsWith('@')) return parts.slice(0, 2).join('/');
182
+ return parts[0] ?? specifier;
183
+ }
184
+
185
+ /** POSIX 路径规整(`a/./b/../c` → `a/c`),保留开头的 `..`。 */
186
+ function normalizePosix(input: string): string {
187
+ const out: string[] = [];
188
+ for (const segment of input.split('/')) {
189
+ if (segment === '' || segment === '.') continue;
190
+ if (segment === '..') {
191
+ if (out.length > 0 && out[out.length - 1] !== '..') out.pop();
192
+ else out.push('..');
193
+ } else {
194
+ out.push(segment);
195
+ }
196
+ }
197
+ return out.length === 0 ? '.' : out.join('/');
198
+ }
199
+
200
+ /** 一个 `require` 说明符的归类;`importer` 是相对源码根目录的路径。 */
201
+ export function classifySpecifier(specifier: string, importer: string): SpecifierClass {
202
+ if (specifier === CONNECTOR_SDK_SPECIFIER) return { kind: 'sdk' };
203
+ const isRelative = specifier === '.' || specifier === '..' || specifier.startsWith('./') || specifier.startsWith('../');
204
+ if (isRelative) {
205
+ const slash = importer.lastIndexOf('/');
206
+ const baseDir = slash === -1 ? '' : importer.slice(0, slash);
207
+ const target = normalizePosix(baseDir ? `${baseDir}/${specifier}` : specifier);
208
+ if (target === '..' || target.startsWith('../')) return { kind: 'escape' };
209
+ return { kind: 'relative', target };
210
+ }
211
+ const sdkProblem = sdkSpecifierProblem(specifier);
212
+ if (sdkProblem !== null) return { kind: 'forbidden', message: sdkProblem };
213
+ if (MACRO_SPECIFIER.test(specifier)) return { kind: 'forbidden', message: `import '${specifier}' is not allowed; connectors cannot use Bun macros` };
214
+ if (specifier.startsWith('node:') || specifier.startsWith('bun:') || specifier === 'bun' || NODE_BUILTINS.has(packageNameOf(specifier))) {
215
+ return { kind: 'forbidden', message: `import '${specifier}' is not allowed; connectors cannot use Node.js or Bun built-in modules` };
216
+ }
217
+ if (specifier.startsWith('/') || /^[a-z][a-z0-9+.-]*:/i.test(specifier)) {
218
+ return { kind: 'forbidden', message: `import '${specifier}' is not allowed; only relative imports and '${CONNECTOR_SDK_SPECIFIER}' are allowed` };
219
+ }
220
+ return { kind: 'forbidden', message: `import '${specifier}' is not allowed; connectors can only depend on '${CONNECTOR_SDK_SPECIFIER}'` };
221
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `@deepexi/datasense-connector/testing`:在作者自己的机器上运行连接器(vitest 与 CLI 共用)。
3
+ *
4
+ * - `loadLocalConnector`:读 `.ontos/descriptor.json`,按虚拟入口契约把源码目录转译成打包串,并收集能力目录下的测试用例;
5
+ * - `runTests`:跑本地连接器的全部测试用例并统计覆盖(`TestReport`);`runConnectorTests` 是只给文件的版本(runner 用);
6
+ * - `createVmExecutor`:在空环境变量的子进程里用与平台 runner 相同的 guest 会话执行(CLI 用);
7
+ * - `runStream` / `runCheck` / `runPush` / `runSuggestColumns`:在当前进程里执行已导入的连接器模块
8
+ * (`{ descriptor, implementations }`,vitest 用);`runOperation(executor, …)`:经任一执行器执行一次连接器操作;
9
+ * - `createLocalGateway`:本地模拟网关,应答 guest 的出站 HTTP、数据库、加密、缓存与凭证调用(主机求交、限流、句柄替换与平台一致);
10
+ * - 用例:`createRecorder` 挂在网关的 `onExchange` 上录制、`serializeFixture` / `parseFixtures` / `readFixtureDir` 读写
11
+ * `<能力目录>/tests/<用例名>/`,`createReplayHost` / `runFixture` / `runFixtures` 不联网回放并做合规检查;
12
+ * - `createMockExternalSystem`:连接器操作单元测试用的模拟外部系统(按路由应答、排好成功 / 拒绝 / 超时,记下请求),
13
+ * 它的 `fetch` 交给 `createLocalGateway({ fetch })`;
14
+ * - 纯函数 `inferRecordSchema`、`checkCompliance`(不依赖 Node 模块)。
15
+ *
16
+ * 连接器源码不要导入本入口:平台打包时只认 `@deepexi/datasense-connector` 本身,子路径一律拒绝。
17
+ */
18
+ export type { GuestJob, HostMethod, LocalHost, LocalRunErrorCode, LocalRunResult, GuestExecutor, SourceLoadErrorCode } from './types';
19
+ export { LocalRunError, SourceLoadError } from './types';
20
+ export type { LocalExchange, LocalGatewayOptions } from './gateway';
21
+ export { createLocalGateway } from './gateway';
22
+ export type { OperationRunInput, StreamRunInput, VmExecutorOptions } from './engine';
23
+ export { createInProcessExecutor, createVmExecutor, runCheck, runOperation, runPush, runStream, runSuggestColumns } from './engine';
24
+ export type { GuestOperationResult } from '../runtime/protocol';
25
+ export type { LocalConnector } from './source-loader';
26
+ export { DESCRIPTOR_FILE, GENERATED_TYPES_FILE, loadLocalConnector } from './source-loader';
27
+ export type { OperationCaseGap, RunTestsOptions, TestReport } from './run-tests';
28
+ export { descriptorCapabilities, operationCaseGaps, runConnectorTests, runTests, testCoverage } from './run-tests';
29
+ export { CREDENTIAL_HEADER_NAMES, redactHeaders, redactQuery, redactSecretValues, redactUrl, secretRedactionTexts } from './redaction';
30
+ export type {
31
+ ConnectorFixtureFile, FixtureCase, FixtureExchange, FixtureExpectation, FixtureRequest, LoadedFixture, OperationFixtureExpectation, SuggestFixtureExpectation,
32
+ } from './fixtures';
33
+ export { FixtureFormatError, parseFixtures, readFixtureDir, serializeFixture } from './fixtures';
34
+ export type { FixtureRecorder } from './recorder';
35
+ export { createRecorder } from './recorder';
36
+ export type { FixtureCaseResult, FixtureMismatch, ReplayHost, RunFixtureOptions } from './replay';
37
+ export { createReplayHost, FixtureReplayError, runFixture, runFixtures } from './replay';
38
+ export type { MockCall, MockExternalSystem, MockRequest, MockResponse, MockRouteHandler, MockStep } from './mock-external-system';
39
+ export { createMockExternalSystem } from './mock-external-system';
40
+ export type { InferredColumnType } from './schema';
41
+ export { inferRecordSchema } from './schema';
42
+ export type { ComplianceCode, ComplianceFinding, ComplianceInput, ComplianceTable } from './compliance';
43
+ export { checkCompliance, COMPLIANCE_CODES } from './compliance';
@@ -0,0 +1,226 @@
1
+ /**
2
+ * 本地 `ctx.db`:用 `postgres` 驱动连作者本机配置的 PostgreSQL,形状与平台的数据库代理一致(`DbQueryResponse` / `DbNextResponse`)。
3
+ *
4
+ * - 每次查询 `reserve()` 一条独占连接,`BEGIN READ ONLY` + `statement_timeout`,服务端游标每批 1000 行;
5
+ * 取完、被关闭或网关关闭时结束事务并归还连接(与 connector `egress/db.ts` 相同)。
6
+ * - 驱动由调用方注入(CLI 静态导入后经 `LocalGatewayOptions.postgres` 传入),否则动态 `import('postgres')`;
7
+ * 导入不了时 `LocalRunError('DB_DRIVER_MISSING')`。SDK 只把 `postgres` 列为可选 peerDependency,不打进 dist。
8
+ */
9
+ import { randomUUID } from 'node:crypto';
10
+ import type { DbNextResponse, DbQueryResponse } from '../runtime/protocol';
11
+ import { redactSecretValues } from './redaction';
12
+ import { LocalRunError } from './types';
13
+
14
+ export interface LocalDbTarget {
15
+ host: string;
16
+ port: number;
17
+ database: string;
18
+ user: string;
19
+ password: string;
20
+ ssl?: boolean;
21
+ }
22
+
23
+ /** 与平台相同的取值(shared 的 `DB_CURSOR_BATCH_ROWS` / `DB_STATEMENT_TIMEOUT_MS`)。 */
24
+ const BATCH_ROWS = 1000;
25
+ const STATEMENT_TIMEOUT_MS = 300_000;
26
+ const APPLICATION_NAME = 'datasense-connector-local';
27
+ /** date、time、timestamp、timetz 按原文返回,避免按本机时区换算。 */
28
+ const RAW_TEXT_OIDS = [1082, 1083, 1114, 1266];
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // postgres.js 用到的最小结构(不依赖它的类型,SDK 的声明文件里不出现 postgres)
32
+
33
+ interface PgRows extends ReadonlyArray<Record<string, unknown>> {
34
+ columns?: Array<{ name: string; type: number }> | null;
35
+ }
36
+ interface PgPending extends PromiseLike<unknown> {
37
+ cursor(rows: number): AsyncIterable<PgRows>;
38
+ }
39
+ interface PgReserved {
40
+ unsafe(text: string, params?: unknown[]): PgPending;
41
+ release(): void;
42
+ }
43
+ interface PgSql {
44
+ reserve(): Promise<PgReserved>;
45
+ end(options?: { timeout?: number }): Promise<void>;
46
+ }
47
+ type PgFactory = (options: Record<string, unknown>) => PgSql;
48
+
49
+ function isRecord(value: unknown): value is Record<string, unknown> {
50
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
51
+ }
52
+
53
+ /** 驱动模块(`import('postgres')` 的结果或 `postgres` 函数本身)→ 工厂函数。 */
54
+ function factoryOf(driver: unknown): PgFactory | null {
55
+ if (typeof driver === 'function') return driver as PgFactory;
56
+ if (isRecord(driver) && typeof driver.default === 'function') return driver.default as PgFactory;
57
+ return null;
58
+ }
59
+
60
+ async function loadDriver(injected: unknown): Promise<PgFactory> {
61
+ if (injected !== undefined && injected !== null) {
62
+ const factory = factoryOf(injected);
63
+ if (!factory) throw new LocalRunError('DB_DRIVER_MISSING', 'the injected postgres driver is not the postgres.js module');
64
+ return factory;
65
+ }
66
+ // 说明符放进变量:打包器与类型检查都不去解析 postgres(它是可选 peerDependency)。
67
+ const specifier = 'postgres';
68
+ let loaded: unknown;
69
+ try {
70
+ loaded = await import(specifier);
71
+ } catch {
72
+ throw new LocalRunError('DB_DRIVER_MISSING', 'ctx.db needs the postgres driver: install it next to the connector (npm install --save-dev postgres)');
73
+ }
74
+ const factory = factoryOf(loaded);
75
+ if (!factory) throw new LocalRunError('DB_DRIVER_MISSING', 'the installed postgres package is not the postgres.js driver');
76
+ return factory;
77
+ }
78
+
79
+ function toParam(value: unknown): unknown {
80
+ if (value === undefined) return null;
81
+ if (isRecord(value)) return JSON.stringify(value);
82
+ return value;
83
+ }
84
+
85
+ function toJsonValue(value: unknown): unknown {
86
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
87
+ if (typeof value === 'bigint') return value.toString();
88
+ if (value instanceof Uint8Array) return Buffer.from(value).toString('base64');
89
+ if (Array.isArray(value)) return value.map(toJsonValue);
90
+ return value;
91
+ }
92
+
93
+ function toRows(rows: ReadonlyArray<Record<string, unknown>>): Array<Record<string, unknown>> {
94
+ return rows.map((row) => {
95
+ const out: Record<string, unknown> = {};
96
+ for (const [name, value] of Object.entries(row)) out[name] = toJsonValue(value);
97
+ return out;
98
+ });
99
+ }
100
+
101
+ /** 带 `code` 的宿主调用失败(与平台数据库代理同码)。 */
102
+ export class LocalDbError extends Error {
103
+ readonly code: string;
104
+
105
+ constructor(code: string, message: string) {
106
+ super(message);
107
+ this.name = 'LocalDbError';
108
+ this.code = code;
109
+ }
110
+ }
111
+
112
+ interface Cursor {
113
+ reserved: PgReserved;
114
+ iterator: AsyncIterator<PgRows>;
115
+ }
116
+
117
+ export interface LocalDb {
118
+ query(request: { sql: string; params: unknown[] }): Promise<DbQueryResponse>;
119
+ next(cursorId: string): Promise<DbNextResponse>;
120
+ close(cursorId: string): Promise<void>;
121
+ end(): Promise<void>;
122
+ }
123
+
124
+ export function createLocalDb(target: LocalDbTarget, options: { driver?: unknown; secrets: readonly string[] }): LocalDb {
125
+ let pool: PgSql | null = null;
126
+ const cursors = new Map<string, Cursor>();
127
+ const secrets = [...options.secrets, target.password];
128
+
129
+ const poolOf = async (): Promise<PgSql> => {
130
+ if (pool) return pool;
131
+ const factory = await loadDriver(options.driver);
132
+ pool = factory({
133
+ host: target.host,
134
+ port: target.port,
135
+ database: target.database,
136
+ username: target.user,
137
+ password: target.password,
138
+ ssl: target.ssl ? 'require' : false,
139
+ max: 4,
140
+ idle_timeout: 60,
141
+ connect_timeout: 10,
142
+ prepare: false,
143
+ onnotice: () => {},
144
+ connection: { application_name: APPLICATION_NAME },
145
+ types: {
146
+ rawText: { to: 25, from: RAW_TEXT_OIDS, serialize: (value: unknown) => String(value), parse: (value: string) => value },
147
+ },
148
+ });
149
+ return pool;
150
+ };
151
+
152
+ const failed = (error: unknown): LocalDbError => {
153
+ if (error instanceof LocalDbError || error instanceof LocalRunError) return error as LocalDbError;
154
+ const message = redactSecretValues(error instanceof Error ? error.message : String(error), secrets);
155
+ return new LocalDbError('DB_PROXY_FAILED', `The query to the source database failed: ${message}`);
156
+ };
157
+
158
+ const finish = async (id: string, cursor: Cursor, ok: boolean): Promise<void> => {
159
+ cursors.delete(id);
160
+ try {
161
+ if (ok) await cursor.iterator.return?.();
162
+ await cursor.reserved.unsafe(ok ? 'commit' : 'rollback');
163
+ } catch {
164
+ await Promise.resolve(cursor.reserved.unsafe('rollback')).then(undefined, () => undefined);
165
+ } finally {
166
+ cursor.reserved.release();
167
+ }
168
+ };
169
+
170
+ return {
171
+ async query(request) {
172
+ const sql = await poolOf();
173
+ let reserved: PgReserved | null = null;
174
+ try {
175
+ reserved = await sql.reserve();
176
+ await reserved.unsafe('begin read only');
177
+ await reserved.unsafe(`set local statement_timeout = ${STATEMENT_TIMEOUT_MS}`);
178
+ // `cursor()` 强制扩展协议(多语句会被服务端拒绝)。
179
+ const iterator = reserved.unsafe(request.sql, request.params.map(toParam)).cursor(BATCH_ROWS)[Symbol.asyncIterator]();
180
+ const first = await iterator.next();
181
+ const rows: PgRows = first.done ? [] : first.value;
182
+ const fields = (rows.columns ?? []).map((column) => ({ name: column.name, dataTypeId: column.type }));
183
+ const id = randomUUID();
184
+ const cursor: Cursor = { reserved, iterator };
185
+ reserved = null;
186
+ if (first.done === true || rows.length < BATCH_ROWS) {
187
+ await finish(id, cursor, true);
188
+ return { cursorId: null, fields, rows: toRows(rows), done: true };
189
+ }
190
+ cursors.set(id, cursor);
191
+ return { cursorId: id, fields, rows: toRows(rows), done: false };
192
+ } catch (error) {
193
+ if (reserved) {
194
+ const held: PgReserved = reserved;
195
+ await Promise.resolve(held.unsafe('rollback')).then(undefined, () => undefined);
196
+ held.release();
197
+ }
198
+ throw failed(error);
199
+ }
200
+ },
201
+ async next(cursorId) {
202
+ const cursor = cursors.get(cursorId);
203
+ if (!cursor) throw new LocalDbError('DB_CURSOR_NOT_FOUND', "The cursor doesn't exist or was closed");
204
+ try {
205
+ const step = await cursor.iterator.next();
206
+ const rows: PgRows = step.done ? [] : step.value;
207
+ const done = step.done === true || rows.length < BATCH_ROWS;
208
+ if (done) await finish(cursorId, cursor, true);
209
+ return { rows: toRows(rows), done };
210
+ } catch (error) {
211
+ await finish(cursorId, cursor, false);
212
+ throw failed(error);
213
+ }
214
+ },
215
+ async close(cursorId) {
216
+ const cursor = cursors.get(cursorId);
217
+ if (cursor) await finish(cursorId, cursor, true);
218
+ },
219
+ async end() {
220
+ for (const [id, cursor] of [...cursors]) await finish(id, cursor, false);
221
+ const current = pool;
222
+ pool = null;
223
+ if (current) await current.end({ timeout: 5 }).catch(() => undefined);
224
+ },
225
+ };
226
+ }