@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.
- package/README.md +158 -139
- package/package.json +19 -28
- package/scripts/build.ts +188 -0
- package/src/define.ts +267 -0
- package/src/descriptor.ts +116 -0
- package/src/errors.ts +105 -0
- package/src/index.ts +108 -0
- package/src/internal/location.ts +23 -0
- package/src/runtime/guest/bridge.ts +238 -0
- package/src/runtime/guest/cache.ts +43 -0
- package/src/runtime/guest/check.ts +17 -0
- package/src/runtime/guest/crypto.ts +166 -0
- package/src/runtime/guest/ctx.ts +157 -0
- package/src/runtime/guest/db.ts +59 -0
- package/src/runtime/guest/failure.ts +69 -0
- package/src/runtime/guest/http.ts +194 -0
- package/src/runtime/guest/index.ts +102 -0
- package/{runtime/guest/limits.d.ts → src/runtime/guest/limits.ts} +12 -12
- package/src/runtime/guest/loader.ts +210 -0
- package/src/runtime/guest/log.ts +47 -0
- package/src/runtime/guest/map.ts +45 -0
- package/src/runtime/guest/operation.ts +193 -0
- package/src/runtime/guest/paginate.ts +169 -0
- package/src/runtime/guest/pull.ts +177 -0
- package/src/runtime/guest/push.ts +92 -0
- package/src/runtime/guest/signal.ts +14 -0
- package/src/runtime/guest/suggest.ts +51 -0
- package/src/runtime/guest/time.ts +137 -0
- package/src/runtime/guest-runtime.ts +75 -0
- package/src/runtime/index.ts +53 -0
- package/src/runtime/protocol.ts +557 -0
- package/src/runtime/session.ts +216 -0
- package/src/testing/child.ts +21 -0
- package/src/testing/compliance.ts +343 -0
- package/src/testing/engine.ts +445 -0
- package/src/testing/fixtures.ts +591 -0
- package/src/testing/gateway.ts +482 -0
- package/src/testing/import-guard.ts +221 -0
- package/src/testing/index.ts +43 -0
- package/src/testing/local-db.ts +226 -0
- package/src/testing/mock-external-system.ts +249 -0
- package/src/testing/pinned-http.ts +144 -0
- package/src/testing/recorder.ts +140 -0
- package/src/testing/redaction.ts +94 -0
- package/src/testing/replay.ts +437 -0
- package/src/testing/run-tests.ts +115 -0
- package/src/testing/schema.ts +70 -0
- package/src/testing/source-loader.ts +374 -0
- package/src/testing/types.ts +78 -0
- package/src/types.ts +329 -0
- package/src/validate.ts +267 -0
- package/test/build.test.ts +159 -0
- package/test/define.test-d.ts +182 -0
- package/test/fixtures/acceptance.ts +156 -0
- package/test/fixtures/connectors/http-pull/.ontos/descriptor.json +72 -0
- package/test/fixtures/connectors/http-pull/.ontos/types.ts +61 -0
- package/test/fixtures/connectors/http-pull/package.json +5 -0
- package/test/fixtures/connectors/http-pull/streams/items/index.ts +37 -0
- package/test/fixtures/connectors/http-pull/streams/items/map.ts +12 -0
- package/test/fixtures/connectors/http-pull/streams/items/tests/basic/case.json +15 -0
- package/test/fixtures/connectors/http-pull/streams/items/tests/basic/exchanges.ndjson +2 -0
- package/test/fixtures/connectors/pg-pull/.ontos/descriptor.json +151 -0
- package/test/fixtures/connectors/pg-pull/.ontos/types.ts +62 -0
- package/test/fixtures/connectors/pg-pull/lib/sql.ts +14 -0
- package/test/fixtures/connectors/pg-pull/package.json +5 -0
- package/test/fixtures/connectors/pg-pull/streams/customers/index.ts +30 -0
- package/test/fixtures/connectors/pg-pull/streams/customers/tests/customers-full/case.json +15 -0
- package/test/fixtures/connectors/pg-pull/streams/customers/tests/customers-full/exchanges.ndjson +3 -0
- package/test/fixtures/connectors/pg-pull/streams/orders/index.ts +32 -0
- package/test/fixtures/descriptor.ts +54 -0
- package/test/fixtures/kingdee-example.ts +82 -0
- package/test/fixtures/record-acceptance.ts +32 -0
- package/test/runtime/ctx.test.ts +434 -0
- package/test/runtime/helpers.ts +157 -0
- package/test/runtime/jobs.test.ts +444 -0
- package/test/runtime/operation.test.ts +175 -0
- package/test/runtime/session.test.ts +229 -0
- package/test/testing/acceptance-fixtures.test.ts +69 -0
- package/test/testing/compliance.test.ts +247 -0
- package/test/testing/engine.test.ts +283 -0
- package/test/testing/fixtures.test.ts +221 -0
- package/test/testing/gateway.test.ts +337 -0
- package/test/testing/import-guard.test.ts +45 -0
- package/test/testing/mock-external-system.test.ts +144 -0
- package/test/testing/operation-fixtures.test.ts +160 -0
- package/test/testing/recorder.test.ts +163 -0
- package/test/testing/redaction.test.ts +54 -0
- package/test/testing/replay.test.ts +207 -0
- package/test/testing/run-tests.test.ts +137 -0
- package/test/testing/source-loader.test.ts +220 -0
- package/test/validate-config.test.ts +104 -0
- package/tsconfig.build.json +23 -0
- package/tsconfig.json +19 -0
- package/cjs/index.cjs +0 -1015
- package/cjs/runtime.cjs +0 -752
- package/cjs/testing.cjs +0 -3591
- package/errors.d.ts +0 -17
- package/errors.js +0 -53
- package/guest.cjs +0 -2659
- package/index.d.ts +0 -13
- package/index.js +0 -7
- package/internal/location.d.ts +0 -6
- package/internal/location.js +0 -8
- package/presentation.d.ts +0 -38
- package/presentation.js +0 -268
- package/runtime/guest/bridge.d.ts +0 -53
- package/runtime/guest/bridge.js +0 -187
- package/runtime/guest/cache.d.ts +0 -3
- package/runtime/guest/cache.js +0 -42
- package/runtime/guest/crypto.d.ts +0 -13
- package/runtime/guest/crypto.js +0 -177
- package/runtime/guest/ctx.d.ts +0 -30
- package/runtime/guest/ctx.js +0 -124
- package/runtime/guest/db.d.ts +0 -6
- package/runtime/guest/db.js +0 -59
- package/runtime/guest/discover.d.ts +0 -29
- package/runtime/guest/discover.js +0 -51
- package/runtime/guest/failure.d.ts +0 -24
- package/runtime/guest/failure.js +0 -63
- package/runtime/guest/http.d.ts +0 -4
- package/runtime/guest/http.js +0 -204
- package/runtime/guest/index.d.ts +0 -12
- package/runtime/guest/index.js +0 -89
- package/runtime/guest/limits.js +0 -12
- package/runtime/guest/loader.d.ts +0 -15
- package/runtime/guest/loader.js +0 -42
- package/runtime/guest/log.d.ts +0 -10
- package/runtime/guest/log.js +0 -36
- package/runtime/guest/map.d.ts +0 -4
- package/runtime/guest/map.js +0 -45
- package/runtime/guest/paginate.d.ts +0 -6
- package/runtime/guest/paginate.js +0 -180
- package/runtime/guest/pull.d.ts +0 -12
- package/runtime/guest/pull.js +0 -150
- package/runtime/guest/push.d.ts +0 -18
- package/runtime/guest/push.js +0 -80
- package/runtime/guest/signal.d.ts +0 -3
- package/runtime/guest/signal.js +0 -12
- package/runtime/guest/time.d.ts +0 -5
- package/runtime/guest/time.js +0 -127
- package/runtime/guest-runtime.d.ts +0 -7
- package/runtime/guest-runtime.js +0 -51
- package/runtime/index.d.ts +0 -18
- package/runtime/index.js +0 -3
- package/runtime/protocol.d.ts +0 -276
- package/runtime/protocol.js +0 -409
- package/runtime/session.d.ts +0 -12
- package/runtime/session.js +0 -191
- package/t.d.ts +0 -18
- package/t.js +0 -26
- package/testing/child.cjs +0 -350
- package/testing/child.d.ts +0 -1
- package/testing/child.js +0 -12
- package/testing/compliance.d.ts +0 -41
- package/testing/compliance.js +0 -291
- package/testing/engine.d.ts +0 -66
- package/testing/engine.js +0 -339
- package/testing/fixtures.d.ts +0 -145
- package/testing/fixtures.js +0 -407
- package/testing/gateway.d.ts +0 -65
- package/testing/gateway.js +0 -402
- package/testing/import-guard.d.ts +0 -57
- package/testing/import-guard.js +0 -187
- package/testing/index.d.ts +0 -33
- package/testing/index.js +0 -10
- package/testing/local-db.d.ts +0 -27
- package/testing/local-db.js +0 -181
- package/testing/pinned-http.d.ts +0 -14
- package/testing/pinned-http.js +0 -125
- package/testing/recorder.d.ts +0 -15
- package/testing/recorder.js +0 -116
- package/testing/redaction.d.ts +0 -28
- package/testing/redaction.js +0 -67
- package/testing/replay.d.ts +0 -50
- package/testing/replay.js +0 -313
- package/testing/schema.d.ts +0 -13
- package/testing/schema.js +0 -65
- package/testing/source-loader.d.ts +0 -19
- package/testing/source-loader.js +0 -238
- package/testing/types.d.ts +0 -79
- package/testing/types.js +0 -18
- package/types.d.ts +0 -302
- package/types.js +0 -13
- package/validate.d.ts +0 -78
- package/validate.js +0 -597
package/cjs/index.cjs
DELETED
|
@@ -1,1015 +0,0 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
-
function __accessProp(key) {
|
|
6
|
-
return this[key];
|
|
7
|
-
}
|
|
8
|
-
var __toCommonJS = (from) => {
|
|
9
|
-
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
10
|
-
if (entry)
|
|
11
|
-
return entry;
|
|
12
|
-
entry = __defProp({}, "__esModule", { value: true });
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (var key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(entry, key))
|
|
16
|
-
__defProp(entry, key, {
|
|
17
|
-
get: __accessProp.bind(from, key),
|
|
18
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
__moduleCache.set(from, entry);
|
|
22
|
-
return entry;
|
|
23
|
-
};
|
|
24
|
-
var __moduleCache;
|
|
25
|
-
var __returnValue = (v) => v;
|
|
26
|
-
function __exportSetter(name, newValue) {
|
|
27
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
28
|
-
}
|
|
29
|
-
var __export = (target, all) => {
|
|
30
|
-
for (var name in all)
|
|
31
|
-
__defProp(target, name, {
|
|
32
|
-
get: all[name],
|
|
33
|
-
enumerable: true,
|
|
34
|
-
configurable: true,
|
|
35
|
-
set: __exportSetter.bind(all, name)
|
|
36
|
-
});
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// src/index.ts
|
|
40
|
-
var exports_src = {};
|
|
41
|
-
__export(exports_src, {
|
|
42
|
-
ConnectorError: () => ConnectorError,
|
|
43
|
-
DEFAULT_PUSH_NAME: () => DEFAULT_PUSH_NAME,
|
|
44
|
-
PNG_DATA_URI_PREFIX: () => PNG_DATA_URI_PREFIX,
|
|
45
|
-
PRESENTATION_LIMITS: () => PRESENTATION_LIMITS,
|
|
46
|
-
RESERVED_PUSH_NAMES: () => RESERVED_PUSH_NAMES,
|
|
47
|
-
SDK_NAME_PATTERN: () => SDK_NAME_PATTERN,
|
|
48
|
-
SDK_VERSION: () => SDK_VERSION,
|
|
49
|
-
SUPPORTED_SDK_MAJORS: () => SUPPORTED_SDK_MAJORS,
|
|
50
|
-
decodeConnectorIcon: () => decodeConnectorIcon,
|
|
51
|
-
defineConnector: () => defineConnector,
|
|
52
|
-
errors: () => errors,
|
|
53
|
-
normalizeDiscoverResult: () => normalizeDiscoverResult,
|
|
54
|
-
readConnectorPresentation: () => readConnectorPresentation,
|
|
55
|
-
summarizeDefinition: () => summarizeDefinition,
|
|
56
|
-
t: () => t,
|
|
57
|
-
validateConfig: () => validateConfig,
|
|
58
|
-
validateConnectorDefinition: () => validateConnectorDefinition,
|
|
59
|
-
validateDiscoverResult: () => validateDiscoverResult
|
|
60
|
-
});
|
|
61
|
-
module.exports = __toCommonJS(exports_src);
|
|
62
|
-
|
|
63
|
-
// src/types.ts
|
|
64
|
-
function isSingleObjectPush(push) {
|
|
65
|
-
if (push === null || typeof push !== "object" || Array.isArray(push))
|
|
66
|
-
return false;
|
|
67
|
-
const value = push;
|
|
68
|
-
return typeof value.map === "function" || typeof value.verify === "function";
|
|
69
|
-
}
|
|
70
|
-
var SINGLE_OBJECT_PUSH_MESSAGE = "push must be declared by name, e.g. push: { default: { map } }; the single-object form push: { map } is not supported";
|
|
71
|
-
function defineConnector(definition) {
|
|
72
|
-
if (definition !== null && typeof definition === "object" && isSingleObjectPush(definition.push)) {
|
|
73
|
-
throw new TypeError(SINGLE_OBJECT_PUSH_MESSAGE);
|
|
74
|
-
}
|
|
75
|
-
return definition;
|
|
76
|
-
}
|
|
77
|
-
// src/t.ts
|
|
78
|
-
var META_KEYS = ["title", "description", "placeholder"];
|
|
79
|
-
function build(base, meta) {
|
|
80
|
-
const out = { ...base };
|
|
81
|
-
if (meta) {
|
|
82
|
-
for (const key of META_KEYS) {
|
|
83
|
-
if (meta[key] !== undefined)
|
|
84
|
-
out[key] = meta[key];
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return Object.freeze(out);
|
|
88
|
-
}
|
|
89
|
-
function scalar(kind, meta) {
|
|
90
|
-
return build({ kind }, meta);
|
|
91
|
-
}
|
|
92
|
-
var t = Object.freeze({
|
|
93
|
-
string: (meta) => scalar("string", meta),
|
|
94
|
-
integer: (meta) => scalar("integer", meta),
|
|
95
|
-
decimal: (precision, scale, meta) => build({ kind: "decimal", precision, scale }, meta),
|
|
96
|
-
boolean: (meta) => scalar("boolean", meta),
|
|
97
|
-
date: (meta) => scalar("date", meta),
|
|
98
|
-
instant: (meta) => scalar("instant", meta),
|
|
99
|
-
json: (meta) => scalar("json", meta),
|
|
100
|
-
object: (shape, meta) => build({ kind: "object", shape: Object.freeze({ ...shape }) }, meta),
|
|
101
|
-
array: (item, meta) => build({ kind: "array", item }, meta),
|
|
102
|
-
secret: (options) => build(options?.exposeToScript === true ? { kind: "secret", exposeToScript: true } : { kind: "secret" }, options)
|
|
103
|
-
});
|
|
104
|
-
// src/errors.ts
|
|
105
|
-
class ConnectorError extends Error {
|
|
106
|
-
code;
|
|
107
|
-
constructor(code, message) {
|
|
108
|
-
super(message);
|
|
109
|
-
this.name = "ConnectorError";
|
|
110
|
-
this.code = code;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
class AuthError extends ConnectorError {
|
|
115
|
-
constructor(message) {
|
|
116
|
-
super("AUTH_ERROR", message);
|
|
117
|
-
this.name = "AuthError";
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
class RateLimited extends ConnectorError {
|
|
122
|
-
retryAfterMs;
|
|
123
|
-
constructor(retryAfterMs, message) {
|
|
124
|
-
super("RATE_LIMITED", message ?? `rate limited, retry after ${retryAfterMs} ms`);
|
|
125
|
-
this.name = "RateLimited";
|
|
126
|
-
this.retryAfterMs = retryAfterMs;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
class TransientError extends ConnectorError {
|
|
131
|
-
constructor(message) {
|
|
132
|
-
super("TRANSIENT_ERROR", message);
|
|
133
|
-
this.name = "TransientError";
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
class SchemaError extends ConnectorError {
|
|
138
|
-
constructor(message) {
|
|
139
|
-
super("SCHEMA_ERROR", message);
|
|
140
|
-
this.name = "SchemaError";
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
class FatalError extends ConnectorError {
|
|
145
|
-
constructor(message) {
|
|
146
|
-
super("FATAL_ERROR", message);
|
|
147
|
-
this.name = "FatalError";
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
class RejectedPayload extends ConnectorError {
|
|
152
|
-
constructor(message) {
|
|
153
|
-
super("REJECTED_PAYLOAD", message);
|
|
154
|
-
this.name = "RejectedPayload";
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
class NotAvailable extends ConnectorError {
|
|
159
|
-
constructor(feature) {
|
|
160
|
-
super("NOT_AVAILABLE", `${feature} is not available yet`);
|
|
161
|
-
this.name = "NotAvailable";
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
var errors = Object.freeze({ AuthError, RateLimited, TransientError, SchemaError, FatalError, RejectedPayload, NotAvailable });
|
|
165
|
-
// src/validate.ts
|
|
166
|
-
var SDK_NAME_PATTERN = /^[a-z][a-z0-9_]{0,27}$/;
|
|
167
|
-
var RESERVED_PUSH_NAMES = Object.freeze(["dev"]);
|
|
168
|
-
var DEFAULT_PUSH_NAME = "default";
|
|
169
|
-
var VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
170
|
-
var SCALAR_KINDS = new Set(["string", "integer", "decimal", "boolean", "date", "instant", "json"]);
|
|
171
|
-
var RETENTION_KINDS = new Set(["instant", "date"]);
|
|
172
|
-
var SYNC_MODES = new Set(["full", "incremental"]);
|
|
173
|
-
var SNAPSHOTS = new Set(["complete", "partial"]);
|
|
174
|
-
var PURGE_KEYS = ["tenant", "user"];
|
|
175
|
-
var FIELD_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
176
|
-
var HOST_PLACEHOLDER = /\{config\.([^{}]*)\}/g;
|
|
177
|
-
var HOST_LABEL = "[a-z0-9](?:[a-z0-9-]*[a-z0-9])?";
|
|
178
|
-
var HOST_PATTERN = new RegExp(`^(?:\\*\\.)?${HOST_LABEL}(?:\\.${HOST_LABEL})*$`, "i");
|
|
179
|
-
var DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
180
|
-
var INSTANT_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
181
|
-
var DECIMAL_TEXT_PATTERN = /^-?\d+(?:\.\d+)?$/;
|
|
182
|
-
var PRESENTATION_LIMITS = Object.freeze({
|
|
183
|
-
title: 40,
|
|
184
|
-
description: 120,
|
|
185
|
-
readmeBytes: 65536,
|
|
186
|
-
iconBytes: 262144,
|
|
187
|
-
iconMinPx: 128,
|
|
188
|
-
fieldTitle: 40,
|
|
189
|
-
fieldDescription: 200,
|
|
190
|
-
placeholder: 100,
|
|
191
|
-
tableTitle: 40,
|
|
192
|
-
tableDescription: 200
|
|
193
|
-
});
|
|
194
|
-
function isRecord(value) {
|
|
195
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
196
|
-
}
|
|
197
|
-
function isPositiveInteger(value) {
|
|
198
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
199
|
-
}
|
|
200
|
-
function isStringArray(value) {
|
|
201
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
202
|
-
}
|
|
203
|
-
function validateText(path, owner, key, max, issues) {
|
|
204
|
-
const value = owner[key];
|
|
205
|
-
if (value === undefined)
|
|
206
|
-
return;
|
|
207
|
-
if (typeof value !== "string") {
|
|
208
|
-
issues.push({ path: `${path}.${key}`, message: `${key} must be a string` });
|
|
209
|
-
} else if ([...value].length > max) {
|
|
210
|
-
issues.push({ path: `${path}.${key}`, message: `${key} must be at most ${max} characters` });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
function validateFieldMeta(path, type, issues) {
|
|
214
|
-
validateText(path, type, "title", PRESENTATION_LIMITS.fieldTitle, issues);
|
|
215
|
-
validateText(path, type, "description", PRESENTATION_LIMITS.fieldDescription, issues);
|
|
216
|
-
if (type.placeholder !== undefined && (type.kind === "object" || type.kind === "array")) {
|
|
217
|
-
issues.push({ path: `${path}.placeholder`, message: "placeholder is only allowed on scalar and secret fields" });
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
validateText(path, type, "placeholder", PRESENTATION_LIMITS.placeholder, issues);
|
|
221
|
-
}
|
|
222
|
-
function columnTypeProblem(type) {
|
|
223
|
-
if (!isRecord(type) || typeof type.kind !== "string" || !SCALAR_KINDS.has(type.kind)) {
|
|
224
|
-
return "column type must be a scalar type (string, integer, decimal, boolean, date, instant or json)";
|
|
225
|
-
}
|
|
226
|
-
if (type.kind === "decimal")
|
|
227
|
-
return decimalProblem(type);
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
function decimalProblem(type) {
|
|
231
|
-
const { precision, scale } = type;
|
|
232
|
-
if (!isPositiveInteger(precision) || typeof scale !== "number" || !Number.isInteger(scale) || scale < 0 || scale > precision) {
|
|
233
|
-
return "decimal needs an integer precision > 0 and an integer scale between 0 and precision";
|
|
234
|
-
}
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
function validateTable(path, table, issues) {
|
|
238
|
-
if (!isRecord(table)) {
|
|
239
|
-
issues.push({ path, message: "table declaration must be an object" });
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const columns = isRecord(table.columns) ? table.columns : null;
|
|
243
|
-
if (!columns) {
|
|
244
|
-
issues.push({ path: `${path}.columns`, message: "columns must be an object" });
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
const columnKind = (name) => {
|
|
248
|
-
const type = columns[name];
|
|
249
|
-
return isRecord(type) && typeof type.kind === "string" ? type.kind : undefined;
|
|
250
|
-
};
|
|
251
|
-
const isDeclared = (name) => typeof name === "string" && Object.hasOwn(columns, name);
|
|
252
|
-
for (const [name, type] of Object.entries(columns)) {
|
|
253
|
-
const problem = columnTypeProblem(type);
|
|
254
|
-
if (problem)
|
|
255
|
-
issues.push({ path: `${path}.columns.${name}`, message: problem });
|
|
256
|
-
else
|
|
257
|
-
validateFieldMeta(`${path}.columns.${name}`, type, issues);
|
|
258
|
-
}
|
|
259
|
-
validateText(path, table, "title", PRESENTATION_LIMITS.tableTitle, issues);
|
|
260
|
-
validateText(path, table, "description", PRESENTATION_LIMITS.tableDescription, issues);
|
|
261
|
-
const primaryKey = table.primaryKey;
|
|
262
|
-
if (!isStringArray(primaryKey) || primaryKey.length === 0) {
|
|
263
|
-
issues.push({ path: `${path}.primaryKey`, message: "primaryKey must be a non-empty list of column names" });
|
|
264
|
-
} else {
|
|
265
|
-
const missing = primaryKey.filter((name) => !isDeclared(name));
|
|
266
|
-
if (missing.length > 0) {
|
|
267
|
-
issues.push({ path: `${path}.primaryKey`, message: `primaryKey references undeclared columns: ${missing.join(", ")}` });
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
if (table.retention !== undefined) {
|
|
271
|
-
const retention = table.retention;
|
|
272
|
-
if (!isRecord(retention)) {
|
|
273
|
-
issues.push({ path: `${path}.retention`, message: "retention must be an object" });
|
|
274
|
-
} else {
|
|
275
|
-
const column = retention.column;
|
|
276
|
-
if (!isDeclared(column)) {
|
|
277
|
-
issues.push({ path: `${path}.retention.column`, message: "retention column must be a declared column" });
|
|
278
|
-
} else if (!RETENTION_KINDS.has(columnKind(column) ?? "")) {
|
|
279
|
-
issues.push({ path: `${path}.retention.column`, message: "retention column must be of type instant or date" });
|
|
280
|
-
} else if (!isStringArray(primaryKey) || !primaryKey.includes(column)) {
|
|
281
|
-
issues.push({ path: `${path}.retention.column`, message: "retention column must be part of primaryKey" });
|
|
282
|
-
}
|
|
283
|
-
if (!isPositiveInteger(retention.days)) {
|
|
284
|
-
issues.push({ path: `${path}.retention.days`, message: "retention days must be a positive integer" });
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
if (table.purgeColumns !== undefined) {
|
|
289
|
-
const purgeColumns = table.purgeColumns;
|
|
290
|
-
if (!isRecord(purgeColumns)) {
|
|
291
|
-
issues.push({ path: `${path}.purgeColumns`, message: "purgeColumns must be an object" });
|
|
292
|
-
} else {
|
|
293
|
-
for (const key of PURGE_KEYS) {
|
|
294
|
-
const column = purgeColumns[key];
|
|
295
|
-
if (column !== undefined && !isDeclared(column)) {
|
|
296
|
-
issues.push({ path: `${path}.purgeColumns.${key}`, message: "purge column must be a declared column" });
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
if (table.indexes !== undefined) {
|
|
302
|
-
const indexes = table.indexes;
|
|
303
|
-
if (!Array.isArray(indexes)) {
|
|
304
|
-
issues.push({ path: `${path}.indexes`, message: "indexes must be a list of column lists" });
|
|
305
|
-
} else {
|
|
306
|
-
indexes.forEach((index, i) => {
|
|
307
|
-
if (!isStringArray(index) || index.length === 0 || !index.every(isDeclared)) {
|
|
308
|
-
issues.push({ path: `${path}.indexes[${i}]`, message: "index must be a non-empty list of declared columns" });
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
function validateTables(path, tables, owners, owner, issues) {
|
|
315
|
-
if (!isRecord(tables)) {
|
|
316
|
-
issues.push({ path, message: "tables must be an object" });
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
for (const [name, table] of Object.entries(tables)) {
|
|
320
|
-
const tablePath = `${path}.${name}`;
|
|
321
|
-
if (!SDK_NAME_PATTERN.test(name)) {
|
|
322
|
-
issues.push({ path: tablePath, message: `table name must match ${SDK_NAME_PATTERN.source}` });
|
|
323
|
-
}
|
|
324
|
-
const previous = owners.get(name);
|
|
325
|
-
if (previous !== undefined) {
|
|
326
|
-
issues.push({ path: tablePath, message: `table ${name} is already declared by ${previous}` });
|
|
327
|
-
} else {
|
|
328
|
-
owners.set(name, owner);
|
|
329
|
-
}
|
|
330
|
-
validateTable(tablePath, table, issues);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
function validateSdkType(path, type, rules, issues) {
|
|
334
|
-
if (!isRecord(type) || typeof type.kind !== "string") {
|
|
335
|
-
issues.push({ path, message: "type must be declared with t.*()" });
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
const nested = { secretAllowed: false, secretMessage: rules.secretMessage };
|
|
339
|
-
if (type.kind === "secret" || type.kind === "object" || type.kind === "array" || SCALAR_KINDS.has(type.kind)) {
|
|
340
|
-
validateFieldMeta(path, type, issues);
|
|
341
|
-
}
|
|
342
|
-
switch (type.kind) {
|
|
343
|
-
case "secret":
|
|
344
|
-
if (!rules.secretAllowed)
|
|
345
|
-
issues.push({ path, message: rules.secretMessage });
|
|
346
|
-
else if (type.exposeToScript !== undefined && typeof type.exposeToScript !== "boolean") {
|
|
347
|
-
issues.push({ path: `${path}.exposeToScript`, message: "exposeToScript must be a boolean" });
|
|
348
|
-
}
|
|
349
|
-
return;
|
|
350
|
-
case "object":
|
|
351
|
-
validateShape(path, type, nested, issues);
|
|
352
|
-
return;
|
|
353
|
-
case "array":
|
|
354
|
-
validateSdkType(`${path}[]`, type.item, nested, issues);
|
|
355
|
-
return;
|
|
356
|
-
case "decimal": {
|
|
357
|
-
const problem = decimalProblem(type);
|
|
358
|
-
if (problem)
|
|
359
|
-
issues.push({ path, message: problem });
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
default:
|
|
363
|
-
if (!SCALAR_KINDS.has(type.kind))
|
|
364
|
-
issues.push({ path, message: `unknown type kind ${type.kind}` });
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
function validateShape(path, type, fieldRules, issues) {
|
|
368
|
-
if (!isRecord(type.shape)) {
|
|
369
|
-
issues.push({ path, message: "object type needs a shape" });
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
for (const [field, fieldType] of Object.entries(type.shape)) {
|
|
373
|
-
const fieldPath = `${path}.${field}`;
|
|
374
|
-
if (!FIELD_NAME_PATTERN.test(field)) {
|
|
375
|
-
issues.push({ path: fieldPath, message: `field name must match ${FIELD_NAME_PATTERN.source}` });
|
|
376
|
-
}
|
|
377
|
-
validateSdkType(fieldPath, fieldType, fieldRules, issues);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
function validateObjectDeclaration(path, type, rules, issues) {
|
|
381
|
-
if (!isRecord(type) || type.kind !== "object") {
|
|
382
|
-
issues.push({ path, message: `${path} must be declared with t.object()` });
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
validateFieldMeta(path, type, issues);
|
|
386
|
-
validateShape(path, type, rules, issues);
|
|
387
|
-
return true;
|
|
388
|
-
}
|
|
389
|
-
function configFieldType(config, fieldPath) {
|
|
390
|
-
let current = config;
|
|
391
|
-
for (const segment of fieldPath.split(".")) {
|
|
392
|
-
if (!isRecord(current) || current.kind !== "object" || !isRecord(current.shape) || !Object.hasOwn(current.shape, segment))
|
|
393
|
-
return;
|
|
394
|
-
current = current.shape[segment];
|
|
395
|
-
}
|
|
396
|
-
return isRecord(current) ? current : undefined;
|
|
397
|
-
}
|
|
398
|
-
function pushNames(push) {
|
|
399
|
-
return isRecord(push) ? Object.keys(push) : [];
|
|
400
|
-
}
|
|
401
|
-
function validatePushHandler(path, handler, issues) {
|
|
402
|
-
if (!isRecord(handler)) {
|
|
403
|
-
issues.push({ path, message: "push handler must be an object with map()" });
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
if (handler.verify !== undefined && typeof handler.verify !== "function") {
|
|
407
|
-
issues.push({ path: `${path}.verify`, message: `${path}.verify must be a function` });
|
|
408
|
-
}
|
|
409
|
-
if (typeof handler.map !== "function") {
|
|
410
|
-
issues.push({ path: `${path}.map`, message: `${path}.map must be a function` });
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
function validatePush(push, issues) {
|
|
414
|
-
if (push === undefined)
|
|
415
|
-
return 0;
|
|
416
|
-
if (!isRecord(push)) {
|
|
417
|
-
issues.push({ path: "push", message: "push must be an object" });
|
|
418
|
-
return 0;
|
|
419
|
-
}
|
|
420
|
-
if (isSingleObjectPush(push)) {
|
|
421
|
-
issues.push({ path: "push", message: SINGLE_OBJECT_PUSH_MESSAGE });
|
|
422
|
-
return 1;
|
|
423
|
-
}
|
|
424
|
-
const names = Object.keys(push);
|
|
425
|
-
if (names.length === 0) {
|
|
426
|
-
issues.push({ path: "push", message: "push must declare at least one handler, e.g. push: { default: { map } }" });
|
|
427
|
-
return 0;
|
|
428
|
-
}
|
|
429
|
-
for (const name of names) {
|
|
430
|
-
const path = `push.${name}`;
|
|
431
|
-
if (!SDK_NAME_PATTERN.test(name)) {
|
|
432
|
-
issues.push({ path, message: `push name must match ${SDK_NAME_PATTERN.source}` });
|
|
433
|
-
} else if (RESERVED_PUSH_NAMES.includes(name)) {
|
|
434
|
-
issues.push({ path, message: `push name ${name} is reserved` });
|
|
435
|
-
}
|
|
436
|
-
validatePushHandler(path, push[name], issues);
|
|
437
|
-
}
|
|
438
|
-
return names.length;
|
|
439
|
-
}
|
|
440
|
-
function validateHosts(hosts, config, issues) {
|
|
441
|
-
if (hosts === undefined)
|
|
442
|
-
return 0;
|
|
443
|
-
if (!Array.isArray(hosts)) {
|
|
444
|
-
issues.push({ path: "hosts", message: "hosts must be a list of host names" });
|
|
445
|
-
return 0;
|
|
446
|
-
}
|
|
447
|
-
hosts.forEach((host, i) => {
|
|
448
|
-
const path = `hosts[${i}]`;
|
|
449
|
-
if (typeof host !== "string" || host.length === 0 || host.length > 253) {
|
|
450
|
-
issues.push({ path, message: "host must be a non-empty string" });
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
for (const match of host.matchAll(HOST_PLACEHOLDER)) {
|
|
454
|
-
const fieldPath = match[1] ?? "";
|
|
455
|
-
const fieldType = configFieldType(config, fieldPath);
|
|
456
|
-
if (!fieldType) {
|
|
457
|
-
issues.push({ path, message: `host template references undeclared config field ${fieldPath}` });
|
|
458
|
-
} else if (fieldType.kind !== "string") {
|
|
459
|
-
issues.push({ path, message: `host template field ${fieldPath} must be declared with t.string()` });
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
if (!HOST_PATTERN.test(host.replace(HOST_PLACEHOLDER, "x"))) {
|
|
463
|
-
issues.push({ path, message: "host must be an exact host name or *.suffix, without scheme, port or path" });
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
return hosts.length;
|
|
467
|
-
}
|
|
468
|
-
function validateRateLimit(rateLimit, issues) {
|
|
469
|
-
if (rateLimit === undefined)
|
|
470
|
-
return;
|
|
471
|
-
if (!isRecord(rateLimit)) {
|
|
472
|
-
issues.push({ path: "rateLimit", message: "rateLimit must be { perSecond, concurrency }" });
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
const { perSecond, concurrency } = rateLimit;
|
|
476
|
-
if (typeof perSecond !== "number" || !Number.isFinite(perSecond) || perSecond <= 0) {
|
|
477
|
-
issues.push({ path: "rateLimit.perSecond", message: "rateLimit.perSecond must be a positive number" });
|
|
478
|
-
}
|
|
479
|
-
if (!isPositiveInteger(concurrency)) {
|
|
480
|
-
issues.push({ path: "rateLimit.concurrency", message: "rateLimit.concurrency must be a positive integer" });
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
function validateStreams(streams, issues) {
|
|
484
|
-
if (streams === undefined)
|
|
485
|
-
return 0;
|
|
486
|
-
if (!isRecord(streams)) {
|
|
487
|
-
issues.push({ path: "streams", message: "streams must be an object of async generator functions" });
|
|
488
|
-
return 0;
|
|
489
|
-
}
|
|
490
|
-
for (const [name, stream] of Object.entries(streams)) {
|
|
491
|
-
const path = `streams.${name}`;
|
|
492
|
-
if (!SDK_NAME_PATTERN.test(name))
|
|
493
|
-
issues.push({ path, message: `stream name must match ${SDK_NAME_PATTERN.source}` });
|
|
494
|
-
if (typeof stream !== "function")
|
|
495
|
-
issues.push({ path, message: `${path} must be an async generator function` });
|
|
496
|
-
}
|
|
497
|
-
return Object.keys(streams).length;
|
|
498
|
-
}
|
|
499
|
-
function validateConnectorDefinition(definition) {
|
|
500
|
-
if (!isRecord(definition)) {
|
|
501
|
-
return [{ path: "", message: "connector definition must be an object" }];
|
|
502
|
-
}
|
|
503
|
-
const issues = [];
|
|
504
|
-
if (typeof definition.name !== "string" || !SDK_NAME_PATTERN.test(definition.name)) {
|
|
505
|
-
issues.push({ path: "name", message: `name must match ${SDK_NAME_PATTERN.source}` });
|
|
506
|
-
}
|
|
507
|
-
if (typeof definition.version !== "string" || !VERSION_PATTERN.test(definition.version)) {
|
|
508
|
-
issues.push({ path: "version", message: "version must be a semver string like 1.0.0" });
|
|
509
|
-
}
|
|
510
|
-
validateObjectDeclaration("config", definition.config, {
|
|
511
|
-
secretAllowed: true,
|
|
512
|
-
secretMessage: "secret fields must be top-level config fields"
|
|
513
|
-
}, issues);
|
|
514
|
-
const hostCount = validateHosts(definition.hosts, definition.config, issues);
|
|
515
|
-
const hasDb = definition.db !== undefined;
|
|
516
|
-
if (hasDb && (!isRecord(definition.db) || definition.db.engine !== "postgresql")) {
|
|
517
|
-
issues.push({ path: "db.engine", message: "db.engine must be 'postgresql'" });
|
|
518
|
-
}
|
|
519
|
-
validateRateLimit(definition.rateLimit, issues);
|
|
520
|
-
if (definition.check !== undefined && typeof definition.check !== "function") {
|
|
521
|
-
issues.push({ path: "check", message: "check must be a function" });
|
|
522
|
-
}
|
|
523
|
-
if (typeof definition.discover !== "function") {
|
|
524
|
-
issues.push({ path: "discover", message: "discover must be a function" });
|
|
525
|
-
}
|
|
526
|
-
if (definition.writes !== undefined) {
|
|
527
|
-
issues.push({ path: "writes", message: "external write operations are not available yet" });
|
|
528
|
-
}
|
|
529
|
-
const streamCount = validateStreams(definition.streams, issues);
|
|
530
|
-
const pushCount = validatePush(definition.push, issues);
|
|
531
|
-
if (streamCount === 0 && pushCount === 0) {
|
|
532
|
-
issues.push({ path: "streams", message: "connector must declare at least one stream or push" });
|
|
533
|
-
}
|
|
534
|
-
if (streamCount > 0 && hostCount === 0 && !hasDb) {
|
|
535
|
-
issues.push({ path: "hosts", message: "connectors with streams must declare hosts (for ctx.http) or db (for ctx.db)" });
|
|
536
|
-
}
|
|
537
|
-
return issues;
|
|
538
|
-
}
|
|
539
|
-
function validateStreamDiscovery(path, stream, owners, issues) {
|
|
540
|
-
if (!isRecord(stream)) {
|
|
541
|
-
issues.push({ path, message: "stream declaration must be an object" });
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
validateTables(`${path}.tables`, stream.tables, owners, path, issues);
|
|
545
|
-
const syncModes = stream.syncModes;
|
|
546
|
-
if (!isStringArray(syncModes) || syncModes.length === 0 || !syncModes.every((mode) => SYNC_MODES.has(mode)) || new Set(syncModes).size !== syncModes.length) {
|
|
547
|
-
issues.push({ path: `${path}.syncModes`, message: "syncModes must be a non-empty list of 'full' and/or 'incremental'" });
|
|
548
|
-
}
|
|
549
|
-
if (stream.snapshot !== undefined && (typeof stream.snapshot !== "string" || !SNAPSHOTS.has(stream.snapshot))) {
|
|
550
|
-
issues.push({ path: `${path}.snapshot`, message: "snapshot must be 'complete' or 'partial'" });
|
|
551
|
-
}
|
|
552
|
-
if (stream.params !== undefined) {
|
|
553
|
-
validateObjectDeclaration(`${path}.params`, stream.params, {
|
|
554
|
-
secretAllowed: false,
|
|
555
|
-
secretMessage: "stream params cannot contain secret fields"
|
|
556
|
-
}, issues);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
function definitionStreamNames(definition) {
|
|
560
|
-
if (!isRecord(definition) || !isRecord(definition.streams))
|
|
561
|
-
return [];
|
|
562
|
-
return Object.keys(definition.streams);
|
|
563
|
-
}
|
|
564
|
-
function definitionPushNames(definition) {
|
|
565
|
-
if (!isRecord(definition))
|
|
566
|
-
return [];
|
|
567
|
-
return pushNames(definition.push);
|
|
568
|
-
}
|
|
569
|
-
function validateDiscoverResult(result, definition) {
|
|
570
|
-
if (!isRecord(result)) {
|
|
571
|
-
return [{ path: "", message: "discover result must be an object" }];
|
|
572
|
-
}
|
|
573
|
-
const issues = [];
|
|
574
|
-
const owners = new Map;
|
|
575
|
-
const declaredStreams = definitionStreamNames(definition);
|
|
576
|
-
const streams = result.streams === undefined ? {} : result.streams;
|
|
577
|
-
if (!isRecord(streams)) {
|
|
578
|
-
issues.push({ path: "streams", message: "streams must be an object" });
|
|
579
|
-
} else {
|
|
580
|
-
for (const [name, stream] of Object.entries(streams)) {
|
|
581
|
-
const path = `streams.${name}`;
|
|
582
|
-
if (!declaredStreams.includes(name)) {
|
|
583
|
-
issues.push({ path, message: `stream ${name} is not defined in the connector's streams` });
|
|
584
|
-
}
|
|
585
|
-
validateStreamDiscovery(path, stream, owners, issues);
|
|
586
|
-
}
|
|
587
|
-
for (const name of declaredStreams) {
|
|
588
|
-
if (!Object.hasOwn(streams, name)) {
|
|
589
|
-
issues.push({ path: `streams.${name}`, message: `stream ${name} is defined but missing from the discover result` });
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
const declaredPushes = definitionPushNames(definition);
|
|
594
|
-
const pushes = result.push === undefined ? {} : result.push;
|
|
595
|
-
if (!isRecord(pushes)) {
|
|
596
|
-
issues.push({ path: "push", message: "push must be an object of { tables } by push name" });
|
|
597
|
-
} else {
|
|
598
|
-
for (const [name, push] of Object.entries(pushes)) {
|
|
599
|
-
const path = `push.${name}`;
|
|
600
|
-
if (!declaredPushes.includes(name)) {
|
|
601
|
-
issues.push({ path, message: `push ${name} is not defined in the connector's push handlers` });
|
|
602
|
-
}
|
|
603
|
-
if (!isRecord(push)) {
|
|
604
|
-
issues.push({ path, message: "push declaration must be { tables }" });
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
validateTables(`${path}.tables`, push.tables, owners, path, issues);
|
|
608
|
-
}
|
|
609
|
-
for (const name of declaredPushes) {
|
|
610
|
-
if (!Object.hasOwn(pushes, name)) {
|
|
611
|
-
issues.push({ path: `push.${name}`, message: `push ${name} is defined but missing from the discover result` });
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
return issues;
|
|
616
|
-
}
|
|
617
|
-
function normalizeDiscoverResult(result) {
|
|
618
|
-
const streams = {};
|
|
619
|
-
for (const [name, stream] of Object.entries(result.streams ?? {})) {
|
|
620
|
-
streams[name] = {
|
|
621
|
-
tables: stream.tables,
|
|
622
|
-
syncModes: [...stream.syncModes],
|
|
623
|
-
snapshot: stream.snapshot ?? "partial",
|
|
624
|
-
...stream.params ? { params: stream.params } : {}
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
const pushes = {};
|
|
628
|
-
for (const [name, push] of Object.entries(result.push ?? {}))
|
|
629
|
-
pushes[name] = { tables: push.tables };
|
|
630
|
-
return { streams, pushes };
|
|
631
|
-
}
|
|
632
|
-
function topLevelSecrets(config) {
|
|
633
|
-
if (!isRecord(config) || !isRecord(config.shape))
|
|
634
|
-
return [];
|
|
635
|
-
const secrets = [];
|
|
636
|
-
for (const [field, type] of Object.entries(config.shape)) {
|
|
637
|
-
if (isRecord(type) && type.kind === "secret")
|
|
638
|
-
secrets.push({ field, exposeToScript: type.exposeToScript === true });
|
|
639
|
-
}
|
|
640
|
-
return secrets;
|
|
641
|
-
}
|
|
642
|
-
function summarizeDefinition(definition) {
|
|
643
|
-
const secrets = topLevelSecrets(definition.config);
|
|
644
|
-
const rateLimit = definition.rateLimit;
|
|
645
|
-
return {
|
|
646
|
-
name: definition.name,
|
|
647
|
-
version: definition.version,
|
|
648
|
-
hosts: Array.isArray(definition.hosts) ? definition.hosts.filter((host) => typeof host === "string") : [],
|
|
649
|
-
db: definition.db?.engine === "postgresql" ? { engine: "postgresql" } : null,
|
|
650
|
-
rateLimit: rateLimit ? { perSecond: rateLimit.perSecond, concurrency: rateLimit.concurrency } : null,
|
|
651
|
-
pushes: pushNames(definition.push),
|
|
652
|
-
streams: definitionStreamNames(definition),
|
|
653
|
-
secretFields: secrets.map((secret) => secret.field),
|
|
654
|
-
exposeToScript: secrets.filter((secret) => secret.exposeToScript).map((secret) => secret.field),
|
|
655
|
-
...isRecord(definition.config) ? { config: JSON.parse(JSON.stringify(definition.config)) } : {}
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
function isValidDate(text) {
|
|
659
|
-
if (!DATE_PATTERN.test(text))
|
|
660
|
-
return false;
|
|
661
|
-
const date = new Date(`${text}T00:00:00Z`);
|
|
662
|
-
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === text;
|
|
663
|
-
}
|
|
664
|
-
function validateValue(path, type, value, issues) {
|
|
665
|
-
if (!isRecord(type) || typeof type.kind !== "string") {
|
|
666
|
-
issues.push({ path, message: "field type is not a valid t.*() declaration" });
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
const fail = (expected) => issues.push({ path, message: `${path} must be ${expected}` });
|
|
670
|
-
switch (type.kind) {
|
|
671
|
-
case "string":
|
|
672
|
-
if (typeof value !== "string")
|
|
673
|
-
fail("a string");
|
|
674
|
-
return;
|
|
675
|
-
case "integer":
|
|
676
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value))
|
|
677
|
-
fail("an integer");
|
|
678
|
-
return;
|
|
679
|
-
case "decimal":
|
|
680
|
-
if (!(typeof value === "number" && Number.isFinite(value)) && !(typeof value === "string" && DECIMAL_TEXT_PATTERN.test(value)))
|
|
681
|
-
fail("a decimal number");
|
|
682
|
-
return;
|
|
683
|
-
case "boolean":
|
|
684
|
-
if (typeof value !== "boolean")
|
|
685
|
-
fail("a boolean");
|
|
686
|
-
return;
|
|
687
|
-
case "date":
|
|
688
|
-
if (typeof value !== "string" || !isValidDate(value))
|
|
689
|
-
fail("a date like 2026-01-31");
|
|
690
|
-
return;
|
|
691
|
-
case "instant":
|
|
692
|
-
if (typeof value !== "string" || !INSTANT_PATTERN.test(value) || Number.isNaN(Date.parse(value)))
|
|
693
|
-
fail("an ISO 8601 timestamp with time zone");
|
|
694
|
-
return;
|
|
695
|
-
case "json":
|
|
696
|
-
return;
|
|
697
|
-
case "object":
|
|
698
|
-
if (!isRecord(value)) {
|
|
699
|
-
fail("an object");
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
validateFields(path, type, value, issues, false);
|
|
703
|
-
return;
|
|
704
|
-
case "array":
|
|
705
|
-
if (!Array.isArray(value)) {
|
|
706
|
-
fail("an array");
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
value.forEach((item, i) => {
|
|
710
|
-
if (item === undefined || item === null)
|
|
711
|
-
issues.push({ path: `${path}[${i}]`, message: `${path}[${i}] is required` });
|
|
712
|
-
else
|
|
713
|
-
validateValue(`${path}[${i}]`, type.item, item, issues);
|
|
714
|
-
});
|
|
715
|
-
return;
|
|
716
|
-
case "secret":
|
|
717
|
-
issues.push({ path, message: "secret fields must be top-level config fields" });
|
|
718
|
-
return;
|
|
719
|
-
default:
|
|
720
|
-
issues.push({ path, message: `unknown type kind ${type.kind}` });
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
function validateFields(path, type, value, issues, topLevel) {
|
|
724
|
-
const shape = isRecord(type.shape) ? type.shape : {};
|
|
725
|
-
for (const [field, fieldType] of Object.entries(shape)) {
|
|
726
|
-
const fieldPath = `${path}.${field}`;
|
|
727
|
-
if (topLevel && isRecord(fieldType) && fieldType.kind === "secret")
|
|
728
|
-
continue;
|
|
729
|
-
const fieldValue = value[field];
|
|
730
|
-
if (fieldValue === undefined || fieldValue === null) {
|
|
731
|
-
if (!(isRecord(fieldType) && fieldType.kind === "json" && fieldValue === null)) {
|
|
732
|
-
issues.push({ path: fieldPath, message: `${fieldPath} is required` });
|
|
733
|
-
}
|
|
734
|
-
continue;
|
|
735
|
-
}
|
|
736
|
-
validateValue(fieldPath, fieldType, fieldValue, issues);
|
|
737
|
-
}
|
|
738
|
-
for (const field of Object.keys(value)) {
|
|
739
|
-
if (!Object.hasOwn(shape, field))
|
|
740
|
-
issues.push({ path: `${path}.${field}`, message: `${path}.${field} is not declared` });
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
function validateConfig(schema, config) {
|
|
744
|
-
if (!isRecord(config))
|
|
745
|
-
return [{ path: "config", message: "config must be an object" }];
|
|
746
|
-
const issues = [];
|
|
747
|
-
validateFields("config", schema, config, issues, true);
|
|
748
|
-
return issues;
|
|
749
|
-
}
|
|
750
|
-
// src/presentation.ts
|
|
751
|
-
var PACKAGE_JSON = "package.json";
|
|
752
|
-
var README = "README.md";
|
|
753
|
-
var ICON_PATHS = ["icon.svg", "icon.png"];
|
|
754
|
-
var ICON_MIME = { "icon.svg": "image/svg+xml", "icon.png": "image/png" };
|
|
755
|
-
var PNG_DATA_URI_PREFIX = "data:image/png;base64,";
|
|
756
|
-
var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
757
|
-
var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
758
|
-
var FORBIDDEN_SVG_ELEMENTS = new Set(["script", "foreignobject", "iframe", "object", "embed"]);
|
|
759
|
-
var ALLOWED_ENTITIES = new Set(["amp", "lt", "gt", "quot", "apos"]);
|
|
760
|
-
var EMPTY_PRESENTATION = { title: null, description: null, icon: null, readme: false };
|
|
761
|
-
function isRecord2(value) {
|
|
762
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
763
|
-
}
|
|
764
|
-
function charCount(text) {
|
|
765
|
-
return [...text].length;
|
|
766
|
-
}
|
|
767
|
-
function utf8Length(text) {
|
|
768
|
-
return new TextEncoder().encode(text).length;
|
|
769
|
-
}
|
|
770
|
-
function readText(owner, key, path, max, issues) {
|
|
771
|
-
const value = owner[key];
|
|
772
|
-
if (value === undefined)
|
|
773
|
-
return null;
|
|
774
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
775
|
-
issues.push({ path, message: `${path.slice(PACKAGE_JSON.length + 1)} must be a non-empty string` });
|
|
776
|
-
return null;
|
|
777
|
-
}
|
|
778
|
-
if (charCount(value) > max) {
|
|
779
|
-
issues.push({ path, message: `${path.slice(PACKAGE_JSON.length + 1)} must be at most ${max} characters` });
|
|
780
|
-
return null;
|
|
781
|
-
}
|
|
782
|
-
return value;
|
|
783
|
-
}
|
|
784
|
-
function scanSvgTags(text) {
|
|
785
|
-
const tags = [];
|
|
786
|
-
let i = 0;
|
|
787
|
-
const n = text.length;
|
|
788
|
-
const isSpace = (ch) => ch === " " || ch === "\t" || ch === `
|
|
789
|
-
` || ch === "\r";
|
|
790
|
-
while (i < n) {
|
|
791
|
-
const lt = text.indexOf("<", i);
|
|
792
|
-
if (lt === -1)
|
|
793
|
-
break;
|
|
794
|
-
if (text.startsWith("<!--", lt)) {
|
|
795
|
-
const end = text.indexOf("-->", lt + 4);
|
|
796
|
-
if (end === -1)
|
|
797
|
-
return null;
|
|
798
|
-
i = end + 3;
|
|
799
|
-
continue;
|
|
800
|
-
}
|
|
801
|
-
if (text.startsWith("<?", lt)) {
|
|
802
|
-
const end = text.indexOf("?>", lt + 2);
|
|
803
|
-
if (end === -1)
|
|
804
|
-
return null;
|
|
805
|
-
i = end + 2;
|
|
806
|
-
continue;
|
|
807
|
-
}
|
|
808
|
-
i = lt + 1;
|
|
809
|
-
if (text[i] === "/") {
|
|
810
|
-
const end = text.indexOf(">", i);
|
|
811
|
-
if (end === -1)
|
|
812
|
-
return null;
|
|
813
|
-
i = end + 1;
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
while (isSpace(text[i]))
|
|
817
|
-
i += 1;
|
|
818
|
-
let name = "";
|
|
819
|
-
while (i < n && !isSpace(text[i]) && text[i] !== ">" && text[i] !== "/")
|
|
820
|
-
name += text[i++];
|
|
821
|
-
const tag = { name: name.slice(name.lastIndexOf(":") + 1), attributes: [] };
|
|
822
|
-
for (;; ) {
|
|
823
|
-
while (isSpace(text[i]))
|
|
824
|
-
i += 1;
|
|
825
|
-
if (i >= n)
|
|
826
|
-
return null;
|
|
827
|
-
if (text[i] === ">") {
|
|
828
|
-
i += 1;
|
|
829
|
-
break;
|
|
830
|
-
}
|
|
831
|
-
if (text[i] === "/") {
|
|
832
|
-
i += 1;
|
|
833
|
-
continue;
|
|
834
|
-
}
|
|
835
|
-
let attr = "";
|
|
836
|
-
while (i < n && !isSpace(text[i]) && text[i] !== "=" && text[i] !== ">" && text[i] !== "/")
|
|
837
|
-
attr += text[i++];
|
|
838
|
-
while (isSpace(text[i]))
|
|
839
|
-
i += 1;
|
|
840
|
-
let value = "";
|
|
841
|
-
if (text[i] === "=") {
|
|
842
|
-
i += 1;
|
|
843
|
-
while (isSpace(text[i]))
|
|
844
|
-
i += 1;
|
|
845
|
-
const quote = text[i];
|
|
846
|
-
if (quote === '"' || quote === "'") {
|
|
847
|
-
const end = text.indexOf(quote, i + 1);
|
|
848
|
-
if (end === -1)
|
|
849
|
-
return null;
|
|
850
|
-
value = text.slice(i + 1, end);
|
|
851
|
-
i = end + 1;
|
|
852
|
-
} else {
|
|
853
|
-
while (i < n && !isSpace(text[i]) && text[i] !== ">")
|
|
854
|
-
value += text[i++];
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
if (attr)
|
|
858
|
-
tag.attributes.push({ name: attr, value });
|
|
859
|
-
}
|
|
860
|
-
tags.push(tag);
|
|
861
|
-
}
|
|
862
|
-
return tags;
|
|
863
|
-
}
|
|
864
|
-
function cssProblem(css) {
|
|
865
|
-
const lower = css.toLowerCase();
|
|
866
|
-
if (lower.includes("url(") || lower.includes("@import"))
|
|
867
|
-
return "must not reference url() or @import in styles";
|
|
868
|
-
if (css.includes("\\"))
|
|
869
|
-
return "must not use escapes in styles";
|
|
870
|
-
return null;
|
|
871
|
-
}
|
|
872
|
-
function svgIconProblem(text) {
|
|
873
|
-
const lower = text.toLowerCase();
|
|
874
|
-
if (lower.includes("<!doctype") || lower.includes("<!entity"))
|
|
875
|
-
return "icon.svg must not contain <!DOCTYPE> or <!ENTITY>";
|
|
876
|
-
if (lower.includes("<![cdata["))
|
|
877
|
-
return "icon.svg must not contain CDATA sections";
|
|
878
|
-
for (const match of lower.matchAll(/&(#?[a-z0-9]*);?/g)) {
|
|
879
|
-
if (!ALLOWED_ENTITIES.has(match[1] ?? "") || !match[0].endsWith(";"))
|
|
880
|
-
return "icon.svg must not contain character or entity references other than & < > " '";
|
|
881
|
-
}
|
|
882
|
-
const tags = scanSvgTags(lower);
|
|
883
|
-
if (tags === null)
|
|
884
|
-
return "icon.svg is not well-formed XML";
|
|
885
|
-
const root = tags[0];
|
|
886
|
-
if (!root || root.name !== "svg")
|
|
887
|
-
return "icon.svg must have an <svg> root element";
|
|
888
|
-
if (!root.attributes.some((attribute) => attribute.name === "viewbox"))
|
|
889
|
-
return "icon.svg root element must have a viewBox";
|
|
890
|
-
for (const tag of tags) {
|
|
891
|
-
if (FORBIDDEN_SVG_ELEMENTS.has(tag.name))
|
|
892
|
-
return `icon.svg must not contain <${tag.name}>`;
|
|
893
|
-
for (const { name, value } of tag.attributes) {
|
|
894
|
-
const local = name.slice(name.lastIndexOf(":") + 1);
|
|
895
|
-
if (local.startsWith("on"))
|
|
896
|
-
return `icon.svg must not contain event handler attributes (${name})`;
|
|
897
|
-
if (local === "href" && !value.trim().startsWith("#"))
|
|
898
|
-
return `icon.svg ${name} must be a local reference starting with #`;
|
|
899
|
-
if (local === "style") {
|
|
900
|
-
const problem = cssProblem(value);
|
|
901
|
-
if (problem)
|
|
902
|
-
return `icon.svg style attribute ${problem}`;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
for (const match of lower.matchAll(/<(?:[a-z0-9_.-]+:)?style\b[^>]*>([\s\S]*?)(?:<\/|$)/g)) {
|
|
907
|
-
const problem = cssProblem(match[1] ?? "");
|
|
908
|
-
if (problem)
|
|
909
|
-
return `icon.svg <style> ${problem}`;
|
|
910
|
-
}
|
|
911
|
-
return null;
|
|
912
|
-
}
|
|
913
|
-
function decodeBase64(text) {
|
|
914
|
-
if (text.length % 4 !== 0 || !BASE64.test(text))
|
|
915
|
-
return null;
|
|
916
|
-
try {
|
|
917
|
-
const binary = atob(text);
|
|
918
|
-
const bytes = new Uint8Array(binary.length);
|
|
919
|
-
for (let i = 0;i < binary.length; i += 1)
|
|
920
|
-
bytes[i] = binary.charCodeAt(i);
|
|
921
|
-
return bytes;
|
|
922
|
-
} catch {
|
|
923
|
-
return null;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
function readUint32(bytes, offset) {
|
|
927
|
-
return (bytes[offset] ?? 0) * 16777216 + ((bytes[offset + 1] ?? 0) << 16 | (bytes[offset + 2] ?? 0) << 8 | (bytes[offset + 3] ?? 0));
|
|
928
|
-
}
|
|
929
|
-
function checkIcon(path, content) {
|
|
930
|
-
const limit = PRESENTATION_LIMITS.iconBytes;
|
|
931
|
-
if (path === "icon.svg") {
|
|
932
|
-
const bytes = new TextEncoder().encode(content);
|
|
933
|
-
if (bytes.length > limit)
|
|
934
|
-
return { problem: `icon.svg must be at most ${limit} bytes` };
|
|
935
|
-
const problem = svgIconProblem(content);
|
|
936
|
-
return problem ? { problem } : { bytes };
|
|
937
|
-
}
|
|
938
|
-
if (!content.startsWith(PNG_DATA_URI_PREFIX))
|
|
939
|
-
return { problem: `icon.png must be stored as ${PNG_DATA_URI_PREFIX}<data>` };
|
|
940
|
-
const encoded = content.slice(PNG_DATA_URI_PREFIX.length);
|
|
941
|
-
if (Math.floor(encoded.length / 4) * 3 - 2 > limit)
|
|
942
|
-
return { problem: `icon.png must be at most ${limit} bytes` };
|
|
943
|
-
const bytes = decodeBase64(encoded);
|
|
944
|
-
if (bytes === null)
|
|
945
|
-
return { problem: "icon.png is not valid base64" };
|
|
946
|
-
if (bytes.length > limit)
|
|
947
|
-
return { problem: `icon.png must be at most ${limit} bytes` };
|
|
948
|
-
if (bytes.length < 24 || PNG_SIGNATURE.some((byte, i) => bytes[i] !== byte) || String.fromCharCode(...bytes.subarray(12, 16)) !== "IHDR") {
|
|
949
|
-
return { problem: "icon.png is not a PNG image" };
|
|
950
|
-
}
|
|
951
|
-
const width = readUint32(bytes, 16);
|
|
952
|
-
const height = readUint32(bytes, 20);
|
|
953
|
-
const min = PRESENTATION_LIMITS.iconMinPx;
|
|
954
|
-
if (width !== height || width < min)
|
|
955
|
-
return { problem: `icon.png must be square and at least ${min}×${min} pixels, got ${width}×${height}` };
|
|
956
|
-
return { bytes };
|
|
957
|
-
}
|
|
958
|
-
function readConnectorPresentation(files, sha256) {
|
|
959
|
-
const issues = [];
|
|
960
|
-
let pkg;
|
|
961
|
-
try {
|
|
962
|
-
pkg = JSON.parse(files[PACKAGE_JSON] ?? "");
|
|
963
|
-
} catch {
|
|
964
|
-
pkg = undefined;
|
|
965
|
-
}
|
|
966
|
-
if (!isRecord2(pkg)) {
|
|
967
|
-
return { presentation: { ...EMPTY_PRESENTATION }, issues: [{ path: PACKAGE_JSON, message: "package.json must be a JSON object" }] };
|
|
968
|
-
}
|
|
969
|
-
const presentation = { ...EMPTY_PRESENTATION };
|
|
970
|
-
presentation.description = readText(pkg, "description", `${PACKAGE_JSON}:description`, PRESENTATION_LIMITS.description, issues);
|
|
971
|
-
const ontos = pkg.ontos;
|
|
972
|
-
if (ontos !== undefined && !isRecord2(ontos)) {
|
|
973
|
-
issues.push({ path: `${PACKAGE_JSON}:ontos`, message: "ontos must be an object" });
|
|
974
|
-
} else if (ontos) {
|
|
975
|
-
presentation.title = readText(ontos, "title", `${PACKAGE_JSON}:ontos.title`, PRESENTATION_LIMITS.title, issues);
|
|
976
|
-
const icon = ontos.icon;
|
|
977
|
-
if (icon !== undefined) {
|
|
978
|
-
if (typeof icon !== "string" || !ICON_PATHS.includes(icon)) {
|
|
979
|
-
issues.push({ path: `${PACKAGE_JSON}:ontos.icon`, message: "ontos.icon must be icon.svg or icon.png (a file in the source root)" });
|
|
980
|
-
} else {
|
|
981
|
-
const path = icon;
|
|
982
|
-
const content = files[path];
|
|
983
|
-
if (content === undefined) {
|
|
984
|
-
issues.push({ path, message: `${path} is referenced by ontos.icon but missing` });
|
|
985
|
-
} else {
|
|
986
|
-
const checked = checkIcon(path, content);
|
|
987
|
-
if ("problem" in checked)
|
|
988
|
-
issues.push({ path, message: checked.problem });
|
|
989
|
-
else
|
|
990
|
-
presentation.icon = { path, mime: ICON_MIME[path], sha256: sha256(checked.bytes), bytes: checked.bytes.length };
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
const readme = files[README];
|
|
996
|
-
if (readme !== undefined) {
|
|
997
|
-
if (utf8Length(readme) > PRESENTATION_LIMITS.readmeBytes) {
|
|
998
|
-
issues.push({ path: README, message: `README.md must be at most ${PRESENTATION_LIMITS.readmeBytes} bytes` });
|
|
999
|
-
} else {
|
|
1000
|
-
presentation.readme = true;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
return { presentation, issues };
|
|
1004
|
-
}
|
|
1005
|
-
function decodeConnectorIcon(files, path) {
|
|
1006
|
-
const content = files[path];
|
|
1007
|
-
if (content === undefined || !ICON_PATHS.includes(path))
|
|
1008
|
-
return null;
|
|
1009
|
-
const checked = checkIcon(path, content);
|
|
1010
|
-
return "bytes" in checked ? checked.bytes : null;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// src/index.ts
|
|
1014
|
-
var SDK_VERSION = "3.0.1";
|
|
1015
|
-
var SUPPORTED_SDK_MAJORS = [3];
|