nucleus-core-ts 0.9.781 → 0.9.783
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/dist/client.js +1 -1
- package/dist/fe/components/IntegrationsPage/components/ConnectionAuthFields.d.ts +2 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionAuthFields.js +85 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionBasicFields.d.ts +2 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionBasicFields.js +165 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionForm.d.ts +10 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionForm.js +112 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionList.d.ts +9 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionList.js +184 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionOauthFields.d.ts +2 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionOauthFields.js +131 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionSettingsTab.d.ts +21 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionSettingsTab.js +49 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionTabs.d.ts +12 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionTabs.js +95 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionTest.d.ts +9 -0
- package/dist/fe/components/IntegrationsPage/components/ConnectionTest.js +116 -0
- package/dist/fe/components/IntegrationsPage/components/CredentialFields.d.ts +9 -0
- package/dist/fe/components/IntegrationsPage/components/CredentialFields.js +171 -0
- package/dist/fe/components/IntegrationsPage/components/CredentialSlotRow.d.ts +13 -0
- package/dist/fe/components/IntegrationsPage/components/CredentialSlotRow.js +63 -0
- package/dist/fe/components/IntegrationsPage/components/EndpointForm.d.ts +11 -0
- package/dist/fe/components/IntegrationsPage/components/EndpointForm.js +166 -0
- package/dist/fe/components/IntegrationsPage/components/EndpointFormFields.d.ts +19 -0
- package/dist/fe/components/IntegrationsPage/components/EndpointFormFields.js +162 -0
- package/dist/fe/components/IntegrationsPage/components/EndpointList.d.ts +12 -0
- package/dist/fe/components/IntegrationsPage/components/EndpointList.js +205 -0
- package/dist/fe/components/IntegrationsPage/components/EndpointSample.d.ts +6 -0
- package/dist/fe/components/IntegrationsPage/components/EndpointSample.js +204 -0
- package/dist/fe/components/IntegrationsPage/components/EndpointsTab.d.ts +5 -0
- package/dist/fe/components/IntegrationsPage/components/EndpointsTab.js +101 -0
- package/dist/fe/components/IntegrationsPage/components/FieldMappingRow.d.ts +17 -0
- package/dist/fe/components/IntegrationsPage/components/FieldMappingRow.js +179 -0
- package/dist/fe/components/IntegrationsPage/components/FieldMappingRows.d.ts +11 -0
- package/dist/fe/components/IntegrationsPage/components/FieldMappingRows.js +115 -0
- package/dist/fe/components/IntegrationsPage/components/IntegrationsPage.d.ts +2 -0
- package/dist/fe/components/IntegrationsPage/components/IntegrationsPage.js +175 -0
- package/dist/fe/components/IntegrationsPage/components/MappingEditor.d.ts +15 -0
- package/dist/fe/components/IntegrationsPage/components/MappingEditor.js +193 -0
- package/dist/fe/components/IntegrationsPage/components/MappingList.d.ts +11 -0
- package/dist/fe/components/IntegrationsPage/components/MappingList.js +191 -0
- package/dist/fe/components/IntegrationsPage/components/MappingWriteModeFields.d.ts +19 -0
- package/dist/fe/components/IntegrationsPage/components/MappingWriteModeFields.js +150 -0
- package/dist/fe/components/IntegrationsPage/components/MappingsTab.d.ts +5 -0
- package/dist/fe/components/IntegrationsPage/components/MappingsTab.js +154 -0
- package/dist/fe/components/IntegrationsPage/components/PaginationFields.d.ts +9 -0
- package/dist/fe/components/IntegrationsPage/components/PaginationFields.js +218 -0
- package/dist/fe/components/IntegrationsPage/components/ParamBindingRow.d.ts +24 -0
- package/dist/fe/components/IntegrationsPage/components/ParamBindingRow.js +180 -0
- package/dist/fe/components/IntegrationsPage/components/ParamBindingRows.d.ts +33 -0
- package/dist/fe/components/IntegrationsPage/components/ParamBindingRows.js +100 -0
- package/dist/fe/components/IntegrationsPage/components/Primitives.d.ts +65 -0
- package/dist/fe/components/IntegrationsPage/components/Primitives.js +94 -0
- package/dist/fe/components/IntegrationsPage/components/RunConfirmStep.d.ts +10 -0
- package/dist/fe/components/IntegrationsPage/components/RunConfirmStep.js +48 -0
- package/dist/fe/components/IntegrationsPage/components/RunHistory.d.ts +10 -0
- package/dist/fe/components/IntegrationsPage/components/RunHistory.js +124 -0
- package/dist/fe/components/IntegrationsPage/components/RunHistoryRow.d.ts +7 -0
- package/dist/fe/components/IntegrationsPage/components/RunHistoryRow.js +129 -0
- package/dist/fe/components/IntegrationsPage/components/RunPanel.d.ts +9 -0
- package/dist/fe/components/IntegrationsPage/components/RunPanel.js +160 -0
- package/dist/fe/components/IntegrationsPage/components/RunPlanIssues.d.ts +5 -0
- package/dist/fe/components/IntegrationsPage/components/RunPlanIssues.js +43 -0
- package/dist/fe/components/IntegrationsPage/components/RunPlanSummary.d.ts +5 -0
- package/dist/fe/components/IntegrationsPage/components/RunPlanSummary.js +92 -0
- package/dist/fe/components/IntegrationsPage/components/RunProgressBar.d.ts +8 -0
- package/dist/fe/components/IntegrationsPage/components/RunProgressBar.js +77 -0
- package/dist/fe/components/IntegrationsPage/components/RunRevertConflicts.d.ts +13 -0
- package/dist/fe/components/IntegrationsPage/components/RunRevertConflicts.js +65 -0
- package/dist/fe/components/IntegrationsPage/components/RunsTab.d.ts +13 -0
- package/dist/fe/components/IntegrationsPage/components/RunsTab.js +39 -0
- package/dist/fe/components/IntegrationsPage/components/Skeletons.d.ts +9 -0
- package/dist/fe/components/IntegrationsPage/components/Skeletons.js +139 -0
- package/dist/fe/components/IntegrationsPage/components/authOptions.d.ts +29 -0
- package/dist/fe/components/IntegrationsPage/components/authOptions.js +71 -0
- package/dist/fe/components/IntegrationsPage/components/connectionDraft.d.ts +64 -0
- package/dist/fe/components/IntegrationsPage/components/connectionDraft.js +112 -0
- package/dist/fe/components/IntegrationsPage/components/credentialSlots.d.ts +25 -0
- package/dist/fe/components/IntegrationsPage/components/credentialSlots.js +67 -0
- package/dist/fe/components/IntegrationsPage/components/fieldMappingOptions.d.ts +16 -0
- package/dist/fe/components/IntegrationsPage/components/fieldMappingOptions.js +41 -0
- package/dist/fe/components/IntegrationsPage/components/formSupport.d.ts +19 -0
- package/dist/fe/components/IntegrationsPage/components/formSupport.js +25 -0
- package/dist/fe/components/IntegrationsPage/components/mappingDraft.d.ts +40 -0
- package/dist/fe/components/IntegrationsPage/components/mappingDraft.js +44 -0
- package/dist/fe/components/IntegrationsPage/components/pathParams.d.ts +9 -0
- package/dist/fe/components/IntegrationsPage/components/pathParams.js +15 -0
- package/dist/fe/components/IntegrationsPage/components/runFormat.d.ts +33 -0
- package/dist/fe/components/IntegrationsPage/components/runFormat.js +85 -0
- package/dist/fe/components/IntegrationsPage/components/useInfiniteList.d.ts +40 -0
- package/dist/fe/components/IntegrationsPage/components/useInfiniteList.js +110 -0
- package/dist/fe/components/IntegrationsPage/index.d.ts +18 -0
- package/dist/fe/components/IntegrationsPage/index.js +11 -0
- package/dist/fe/components/IntegrationsPage/store/index.d.ts +2 -0
- package/dist/fe/components/IntegrationsPage/store/index.js +124 -0
- package/dist/fe/components/IntegrationsPage/store/state.d.ts +80 -0
- package/dist/fe/components/IntegrationsPage/store/state.js +32 -0
- package/dist/fe/components/IntegrationsPage/theme/index.d.ts +103 -0
- package/dist/fe/components/IntegrationsPage/theme/index.js +113 -0
- package/dist/fe/components/IntegrationsPage/types/actions.d.ts +91 -0
- package/dist/fe/components/IntegrationsPage/types/actions.js +1 -0
- package/dist/fe/components/IntegrationsPage/types/index.d.ts +3 -0
- package/dist/fe/components/IntegrationsPage/types/index.js +3 -0
- package/dist/fe/components/IntegrationsPage/types/records.d.ts +139 -0
- package/dist/fe/components/IntegrationsPage/types/records.js +11 -0
- package/dist/fe/components/IntegrationsPage/types/results.d.ts +52 -0
- package/dist/fe/components/IntegrationsPage/types/results.js +4 -0
- package/dist/fe/index.d.ts +2 -0
- package/dist/fe/index.js +1 -0
- package/dist/index.js +14 -14
- package/dist/src/Client/ApiCaller/system-tables.d.ts +505 -0
- package/dist/src/Client/Proxy/wsProxy.js +25 -1
- package/dist/src/ElysiaPlugin/routes/backup/types.d.ts +3 -1
- package/dist/src/ElysiaPlugin/routes/integrations/index.d.ts +49 -0
- package/dist/src/ElysiaPlugin/routes/integrations/inspect.d.ts +87 -0
- package/dist/src/ElysiaPlugin/routes/integrations/inspectRoutes.d.ts +41 -0
- package/dist/src/ElysiaPlugin/routes/integrations/repository.d.ts +88 -0
- package/dist/src/ElysiaPlugin/routes/integrations/types.d.ts +22 -0
- package/dist/src/ElysiaPlugin/routes/pubsub/frame.d.ts +37 -0
- package/dist/src/ElysiaPlugin/routes/pubsub/frame.test.d.ts +1 -0
- package/dist/src/Services/Backup/BackupService.d.ts +16 -8
- package/dist/src/Services/Backup/types.d.ts +25 -0
- package/dist/src/Services/Integrations/bindings.d.ts +116 -0
- package/dist/src/Services/Integrations/bindings.test.d.ts +1 -0
- package/dist/src/Services/Integrations/collect.d.ts +68 -0
- package/dist/src/Services/Integrations/collect.test.d.ts +1 -0
- package/dist/src/Services/Integrations/fetcher.d.ts +100 -0
- package/dist/src/Services/Integrations/fetcher.test.d.ts +1 -0
- package/dist/src/Services/Integrations/lock.d.ts +77 -0
- package/dist/src/Services/Integrations/lock.test.d.ts +1 -0
- package/dist/src/Services/Integrations/mapping.d.ts +63 -0
- package/dist/src/Services/Integrations/mapping.test.d.ts +1 -0
- package/dist/src/Services/Integrations/oauth.d.ts +89 -0
- package/dist/src/Services/Integrations/oauth.test.d.ts +1 -0
- package/dist/src/Services/Integrations/pagination.d.ts +97 -0
- package/dist/src/Services/Integrations/pagination.test.d.ts +1 -0
- package/dist/src/Services/Integrations/plan.d.ts +74 -0
- package/dist/src/Services/Integrations/plan.test.d.ts +1 -0
- package/dist/src/Services/Integrations/revert.d.ts +78 -0
- package/dist/src/Services/Integrations/revert.test.d.ts +1 -0
- package/dist/src/Services/Integrations/runner.d.ts +126 -0
- package/dist/src/Services/Integrations/runner.test.d.ts +1 -0
- package/dist/src/Services/Integrations/types.d.ts +208 -0
- package/dist/src/Services/Integrations/writer.d.ts +105 -0
- package/dist/src/Services/Integrations/writer.test.d.ts +1 -0
- package/dist/src/Utils/cron.d.ts +65 -0
- package/dist/src/Utils/cron.test.d.ts +1 -0
- package/dist/src/types.d.ts +67 -1
- package/package.json +1 -1
- package/schemas/config.nucleus.json +43 -0
- package/schemas/table.schema.json +2 -1
- package/scripts/generate-schema.test.ts +78 -0
- package/scripts/generate-schema.ts +43 -5
- package/src/system.tables.json +362 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the data-integration engine.
|
|
3
|
+
*
|
|
4
|
+
* The engine pulls records from a third-party HTTP API and writes them into one
|
|
5
|
+
* of this app's own entities. Everything here describes that translation; none of
|
|
6
|
+
* it knows which customer, which API, or which table — a source is whatever an
|
|
7
|
+
* operator registered.
|
|
8
|
+
*/
|
|
9
|
+
/** A per-field transform applied to a source value before it lands in a column. */
|
|
10
|
+
export type MappingTransform = 'none' | 'trim' | 'upper' | 'lower' | 'toString' | 'toNumber' | 'toBoolean' | 'dateOnly';
|
|
11
|
+
/** One field of a mapping: external `source` path → internal `target` column. */
|
|
12
|
+
export type FieldMapping = {
|
|
13
|
+
/** Dot-path into the external record, e.g. `employeeNo` or `department.code`. */
|
|
14
|
+
source: string;
|
|
15
|
+
/** Target column, camelCase, e.g. `code` or `locationCode`. */
|
|
16
|
+
target: string;
|
|
17
|
+
transform?: MappingTransform;
|
|
18
|
+
/** Used INSTEAD of reading `source` — a fixed value for every row. */
|
|
19
|
+
constant?: string;
|
|
20
|
+
/** Used when the source value resolves to null/undefined/empty. */
|
|
21
|
+
fallback?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Where one path/query parameter of an endpoint gets its value.
|
|
25
|
+
*
|
|
26
|
+
* `endpoint` is the interesting one: the parent endpoint is fetched first and the
|
|
27
|
+
* child is called once per parent record. That is how an API that exposes
|
|
28
|
+
* `/thing/{id}` and nothing else can still be imported in full.
|
|
29
|
+
*
|
|
30
|
+
* `entity` closes the other half — the values may already be in OUR table, e.g.
|
|
31
|
+
* "for every employee I have, fetch their detail record".
|
|
32
|
+
*/
|
|
33
|
+
export type ParamBinding = {
|
|
34
|
+
param: string;
|
|
35
|
+
from: 'static';
|
|
36
|
+
value: string;
|
|
37
|
+
} | {
|
|
38
|
+
param: string;
|
|
39
|
+
from: 'endpoint';
|
|
40
|
+
endpointId: string;
|
|
41
|
+
field: string;
|
|
42
|
+
} | {
|
|
43
|
+
param: string;
|
|
44
|
+
from: 'entity';
|
|
45
|
+
entity: string;
|
|
46
|
+
field: string;
|
|
47
|
+
};
|
|
48
|
+
/** A resolved parameter set for ONE child call, plus the parent row it came from. */
|
|
49
|
+
export type ResolvedParams = {
|
|
50
|
+
values: Record<string, unknown>;
|
|
51
|
+
/**
|
|
52
|
+
* The parent record this call was derived from, so a mapping can write parent
|
|
53
|
+
* fields onto the child's rows (`__parent.name`). Absent for a root endpoint.
|
|
54
|
+
*/
|
|
55
|
+
parent?: unknown;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* How an endpoint pages, chosen explicitly by the operator.
|
|
59
|
+
*
|
|
60
|
+
* Never inferred. A wrong guess reads page one and reports success, and if the
|
|
61
|
+
* mapping is in `replace` mode that silently destroys everything the first page
|
|
62
|
+
* did not contain.
|
|
63
|
+
*/
|
|
64
|
+
export type PaginationProfile = {
|
|
65
|
+
kind: 'none';
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'page_number';
|
|
68
|
+
pageParam: string;
|
|
69
|
+
sizeParam?: string;
|
|
70
|
+
pageSize?: number;
|
|
71
|
+
/** First page's number — 0 for Spring, 1 for most others. */
|
|
72
|
+
firstPage?: number;
|
|
73
|
+
/** Dot-path to the total RECORD count in the response, if the API reports one. */
|
|
74
|
+
totalPath?: string;
|
|
75
|
+
/** Dot-path to a boolean saying this is the final page, if the API reports one. */
|
|
76
|
+
lastPagePath?: string;
|
|
77
|
+
} | {
|
|
78
|
+
kind: 'offset';
|
|
79
|
+
offsetParam: string;
|
|
80
|
+
limitParam: string;
|
|
81
|
+
pageSize?: number;
|
|
82
|
+
totalPath?: string;
|
|
83
|
+
} | {
|
|
84
|
+
kind: 'cursor';
|
|
85
|
+
cursorParam: string;
|
|
86
|
+
/** Dot-path to the next cursor in the response. Absent/empty ends the walk. */
|
|
87
|
+
nextCursorPath: string;
|
|
88
|
+
sizeParam?: string;
|
|
89
|
+
pageSize?: number;
|
|
90
|
+
} | {
|
|
91
|
+
kind: 'link_header';
|
|
92
|
+
sizeParam?: string;
|
|
93
|
+
pageSize?: number;
|
|
94
|
+
};
|
|
95
|
+
/** One page's worth of the walk: what came back, and whether to keep going. */
|
|
96
|
+
export type PageOutcome = {
|
|
97
|
+
records: unknown[];
|
|
98
|
+
/** Params to add to the NEXT request, or null when the walk is finished. */
|
|
99
|
+
nextParams: Record<string, string> | null;
|
|
100
|
+
/** Total the source claims to hold, when it reports one. */
|
|
101
|
+
reportedTotal: number | null;
|
|
102
|
+
};
|
|
103
|
+
/** A mapped internal row plus the provenance needed to report on it. */
|
|
104
|
+
export type MappedRecord = {
|
|
105
|
+
/** Row payload (camelCase target fields), ready for validation. */
|
|
106
|
+
data: Record<string, unknown>;
|
|
107
|
+
/** Value of the dedup column, when the mapping configures one. */
|
|
108
|
+
dedupKey: string | null;
|
|
109
|
+
/** Zero-based index in the fetched set, so an issue can name the record. */
|
|
110
|
+
sourceIndex: number;
|
|
111
|
+
};
|
|
112
|
+
export type WriteMode = 'upsert' | 'append' | 'replace';
|
|
113
|
+
export type MissingRecordPolicy = 'ignore' | 'deactivate' | 'delete';
|
|
114
|
+
export type RunStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'reverted';
|
|
115
|
+
/**
|
|
116
|
+
* A problem with ONE source record.
|
|
117
|
+
*
|
|
118
|
+
* Distinct from a run-level error: issues describe records, errors describe the
|
|
119
|
+
* run. A run that skips two records for missing fields has two issues and no
|
|
120
|
+
* errors, and it still succeeds.
|
|
121
|
+
*/
|
|
122
|
+
export type RunIssue = {
|
|
123
|
+
sourceIndex: number;
|
|
124
|
+
kind: 'missing_required' | 'missing_dedup_key' | 'duplicate_in_batch' | 'invalid_value' | 'unmapped';
|
|
125
|
+
message: string;
|
|
126
|
+
dedupKey?: string | null;
|
|
127
|
+
};
|
|
128
|
+
/** Why a run refused to start or had to stop. */
|
|
129
|
+
export type RunFailure = {
|
|
130
|
+
kind: 'fetch_failed';
|
|
131
|
+
message: string;
|
|
132
|
+
} | {
|
|
133
|
+
kind: 'validation_failed';
|
|
134
|
+
message: string;
|
|
135
|
+
issues: RunIssue[];
|
|
136
|
+
} | {
|
|
137
|
+
kind: 'incomplete_source';
|
|
138
|
+
message: string;
|
|
139
|
+
fetched: number;
|
|
140
|
+
reported: number;
|
|
141
|
+
} | {
|
|
142
|
+
kind: 'too_many_records';
|
|
143
|
+
message: string;
|
|
144
|
+
fetched: number;
|
|
145
|
+
limit: number;
|
|
146
|
+
} | {
|
|
147
|
+
kind: 'cycle_detected';
|
|
148
|
+
message: string;
|
|
149
|
+
chain: string[];
|
|
150
|
+
} | {
|
|
151
|
+
kind: 'already_running';
|
|
152
|
+
message: string;
|
|
153
|
+
} | {
|
|
154
|
+
kind: 'cancelled';
|
|
155
|
+
message: string;
|
|
156
|
+
} | {
|
|
157
|
+
kind: 'write_failed';
|
|
158
|
+
message: string;
|
|
159
|
+
};
|
|
160
|
+
/** What a run would do, computed without writing anything. */
|
|
161
|
+
export type RunPlan = {
|
|
162
|
+
targetEntity: string;
|
|
163
|
+
writeMode: WriteMode;
|
|
164
|
+
totalFetched: number;
|
|
165
|
+
reportedTotal: number | null;
|
|
166
|
+
toCreate: number;
|
|
167
|
+
toUpdate: number;
|
|
168
|
+
toSkip: number;
|
|
169
|
+
/** Rows here but no longer in the source, and what the policy does with them. */
|
|
170
|
+
missing: number;
|
|
171
|
+
missingPolicy: MissingRecordPolicy;
|
|
172
|
+
existingCount: number;
|
|
173
|
+
dedupConfigured: boolean;
|
|
174
|
+
issues: RunIssue[];
|
|
175
|
+
warnings: string[];
|
|
176
|
+
/** A few mapped rows so an operator can eyeball the mapping before committing. */
|
|
177
|
+
sample: Record<string, unknown>[];
|
|
178
|
+
};
|
|
179
|
+
/** The outcome of an applied run. */
|
|
180
|
+
export type RunReport = {
|
|
181
|
+
runId: string;
|
|
182
|
+
status: RunStatus;
|
|
183
|
+
targetEntity: string;
|
|
184
|
+
writeMode: WriteMode;
|
|
185
|
+
totalFetched: number;
|
|
186
|
+
reportedTotal: number | null;
|
|
187
|
+
createdCount: number;
|
|
188
|
+
updatedCount: number;
|
|
189
|
+
skippedCount: number;
|
|
190
|
+
deletedCount: number;
|
|
191
|
+
deactivatedCount: number;
|
|
192
|
+
externalCallCount: number;
|
|
193
|
+
issues: RunIssue[];
|
|
194
|
+
errors: string[];
|
|
195
|
+
failure: RunFailure | null;
|
|
196
|
+
/** False once the before-images have aged out. */
|
|
197
|
+
revertible: boolean;
|
|
198
|
+
};
|
|
199
|
+
/** Progress published on the run's realtime topic while it works. */
|
|
200
|
+
export type RunProgress = {
|
|
201
|
+
runId: string;
|
|
202
|
+
phase: 'fetching' | 'mapping' | 'validating' | 'writing' | 'done';
|
|
203
|
+
/** Records seen so far. Absent when the phase has no meaningful count. */
|
|
204
|
+
processed?: number;
|
|
205
|
+
/** Total the engine expects, when it knows it. */
|
|
206
|
+
total?: number;
|
|
207
|
+
message?: string;
|
|
208
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applying a run's writes — all of them, or none of them.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists at all
|
|
5
|
+
*
|
|
6
|
+
* The engine this replaces wrote through its own HTTP API, looping back to
|
|
7
|
+
* `127.0.0.1` for every batch. A ten-thousand-record replace became roughly a
|
|
8
|
+
* hundred loopback requests, and nucleus opened and committed a SEPARATE
|
|
9
|
+
* transaction for each one. So a failure at request forty left thirty-nine
|
|
10
|
+
* batches permanently written, with nothing to roll back to — the table half
|
|
11
|
+
* deleted and half rewritten, and a report that said "success" because the
|
|
12
|
+
* caller only ever checked HTTP status codes.
|
|
13
|
+
*
|
|
14
|
+
* Here the writes go straight to the database inside ONE transaction. Atomicity
|
|
15
|
+
* stops being something to arrange and becomes the default: any throw unwinds
|
|
16
|
+
* everything.
|
|
17
|
+
*
|
|
18
|
+
* ## The ordering that makes it safe
|
|
19
|
+
*
|
|
20
|
+
* 1. Validate EVERY payload — before the transaction opens. A single invalid row
|
|
21
|
+
* aborts the run having written nothing. Doing this inside the transaction is
|
|
22
|
+
* what let a partially-applied batch commit elsewhere in this codebase, and
|
|
23
|
+
* the mistake is easy to repeat: returning early from inside a drizzle
|
|
24
|
+
* transaction callback COMMITS, because only a throw rolls back.
|
|
25
|
+
* 2. Read the before-images of everything about to change, so a run that lands
|
|
26
|
+
* correctly can still be undone later.
|
|
27
|
+
* 3. Write, inside the transaction, deletes first so a unique constraint freed by
|
|
28
|
+
* a delete is available to an insert in the same run.
|
|
29
|
+
*
|
|
30
|
+
* ## Ownership
|
|
31
|
+
*
|
|
32
|
+
* `scopeConditions` come from the caller's own claims and are ANDed onto every
|
|
33
|
+
* update and delete. A scoped grant must not become "any row" just because the
|
|
34
|
+
* write arrived through the integration engine rather than the entity routes.
|
|
35
|
+
*/
|
|
36
|
+
import { type SQL } from 'drizzle-orm';
|
|
37
|
+
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
38
|
+
import type { NucleusColumn } from '../../types';
|
|
39
|
+
import type { Logger } from '../Logger';
|
|
40
|
+
import type { WriteOperations } from './plan';
|
|
41
|
+
import type { RunFailure, RunIssue } from './types';
|
|
42
|
+
type DrizzleTable = any;
|
|
43
|
+
export type ValidatePayload = (payload: Record<string, unknown>, columns: NucleusColumn[], isPartial?: boolean) => {
|
|
44
|
+
valid: boolean;
|
|
45
|
+
errors: {
|
|
46
|
+
field: string;
|
|
47
|
+
message: string;
|
|
48
|
+
}[];
|
|
49
|
+
};
|
|
50
|
+
export type WriterDeps = {
|
|
51
|
+
db: NodePgDatabase;
|
|
52
|
+
drizzleTable: DrizzleTable;
|
|
53
|
+
/** Maps a field name to its drizzle column, or undefined when it does not exist. */
|
|
54
|
+
resolveCol: (field: string) => unknown;
|
|
55
|
+
columns: NucleusColumn[];
|
|
56
|
+
logger: Logger;
|
|
57
|
+
/** Injected so this module does not import the plugin's util barrel. */
|
|
58
|
+
validatePayload: ValidatePayload;
|
|
59
|
+
};
|
|
60
|
+
/** What a row looked like before this run touched it. */
|
|
61
|
+
export type PreImage = {
|
|
62
|
+
targetEntity: string;
|
|
63
|
+
rowId: string | null;
|
|
64
|
+
action: 'create' | 'update' | 'delete' | 'deactivate';
|
|
65
|
+
before: Record<string, unknown> | null;
|
|
66
|
+
after: Record<string, unknown> | null;
|
|
67
|
+
};
|
|
68
|
+
export type ApplyInput = {
|
|
69
|
+
ops: WriteOperations;
|
|
70
|
+
targetEntity: string;
|
|
71
|
+
userId: string;
|
|
72
|
+
/** Ownership predicates from the caller's claims. ANDed onto updates/deletes. */
|
|
73
|
+
scopeConditions?: SQL[];
|
|
74
|
+
/** Column and value written to rows the source no longer carries. */
|
|
75
|
+
missingColumn?: string | null;
|
|
76
|
+
missingValue?: string | null;
|
|
77
|
+
/** Checked between statements so a cancel actually stops the work. */
|
|
78
|
+
isCancelled?: () => boolean;
|
|
79
|
+
};
|
|
80
|
+
export type ApplyResult = {
|
|
81
|
+
ok: true;
|
|
82
|
+
created: number;
|
|
83
|
+
updated: number;
|
|
84
|
+
deleted: number;
|
|
85
|
+
deactivated: number;
|
|
86
|
+
preImages: PreImage[];
|
|
87
|
+
} | {
|
|
88
|
+
ok: false;
|
|
89
|
+
failure: RunFailure;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Validates every payload the run would write.
|
|
93
|
+
*
|
|
94
|
+
* Deliberately separate and called FIRST: an operator finding out at record 5000
|
|
95
|
+
* that record 12 was invalid, with 4999 rows already committed, is the failure
|
|
96
|
+
* this ordering exists to prevent.
|
|
97
|
+
*/
|
|
98
|
+
export declare function validateAll(ops: WriteOperations, columns: NucleusColumn[], validatePayload: ValidatePayload): RunIssue[];
|
|
99
|
+
/**
|
|
100
|
+
* Executes the plan. Returns a failure instead of throwing for the cases a caller
|
|
101
|
+
* must report (invalid data, cancellation); a genuine database error unwinds the
|
|
102
|
+
* transaction and comes back as `write_failed`.
|
|
103
|
+
*/
|
|
104
|
+
export declare function applyWrites(deps: WriterDeps, input: ApplyInput): Promise<ApplyResult>;
|
|
105
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A standard five-field cron evaluator: "when does this expression next fire?"
|
|
3
|
+
*
|
|
4
|
+
* Written because the backup scheduler had its own parser that read the minute
|
|
5
|
+
* and hour and silently discarded day-of-month, month and day-of-week
|
|
6
|
+
* (`Services/Backup/BackupService.ts`, `getNextDelayMs`). `0 2 * * 0` — a job
|
|
7
|
+
* an operator wrote to mean "Sundays at 02:00" — ran every night. Nothing
|
|
8
|
+
* errored, and the only symptom was seven times the expected number of backups.
|
|
9
|
+
*
|
|
10
|
+
* Kept as a general utility rather than a backup detail: any scheduled work in
|
|
11
|
+
* nucleus needs the same answer, and a second hand-rolled parser is how the
|
|
12
|
+
* first one's bug gets recreated.
|
|
13
|
+
*
|
|
14
|
+
* ## Time zones
|
|
15
|
+
*
|
|
16
|
+
* `nextCronRun` interprets the expression in an IANA zone, defaulting to UTC.
|
|
17
|
+
* The old scheduler used `Date.setHours`, i.e. the container's local zone —
|
|
18
|
+
* which in these deployments is UTC, so an operator in Istanbul asking for
|
|
19
|
+
* 02:00 got 05:00 local and had no way to say otherwise.
|
|
20
|
+
*
|
|
21
|
+
* An unknown zone falls back to UTC instead of throwing: a typo in a config
|
|
22
|
+
* string must not stop a service from booting.
|
|
23
|
+
*
|
|
24
|
+
* ## What is supported
|
|
25
|
+
*
|
|
26
|
+
* Five fields with `*`, values, `a-b` ranges, `/step`, and comma lists; the
|
|
27
|
+
* three-letter month and day names (`JAN`, `MON`); and the `@daily`-family
|
|
28
|
+
* macros. Names and macros are here because refusing them is not a neutral act:
|
|
29
|
+
* a caller that treats null as "do not schedule" then stops scheduling
|
|
30
|
+
* altogether, which is how supporting fewer forms turned into taking no backups.
|
|
31
|
+
*
|
|
32
|
+
* ## What is not
|
|
33
|
+
*
|
|
34
|
+
* `L`, `W`, `#`, `@reboot`, and second-resolution (six-field) expressions.
|
|
35
|
+
* `parseCron` returns null for these rather than guessing. A caller must decide
|
|
36
|
+
* what null means for it — and for anything whose job is to run periodically,
|
|
37
|
+
* the safe reading is "fall back to a known-good schedule and say so loudly",
|
|
38
|
+
* not "do nothing".
|
|
39
|
+
*/
|
|
40
|
+
export type CronFields = {
|
|
41
|
+
minute: Set<number>;
|
|
42
|
+
hour: Set<number>;
|
|
43
|
+
/** Day of month, 1-31. */
|
|
44
|
+
day: Set<number>;
|
|
45
|
+
/** Month, 1-12. */
|
|
46
|
+
month: Set<number>;
|
|
47
|
+
/** Day of week, 0-6 with 0 = Sunday. A `7` in the expression normalizes to 0. */
|
|
48
|
+
weekday: Set<number>;
|
|
49
|
+
/** True when the field was literally `*`, which decides the day-of-month/weekday OR. */
|
|
50
|
+
dayUnrestricted: boolean;
|
|
51
|
+
weekdayUnrestricted: boolean;
|
|
52
|
+
};
|
|
53
|
+
/** Parses a five-field expression or an `@`-macro; null when not fully understood. */
|
|
54
|
+
export declare function parseCron(expression: string): CronFields | null;
|
|
55
|
+
/**
|
|
56
|
+
* The next instant strictly after `from` at which `expression` fires.
|
|
57
|
+
*
|
|
58
|
+
* Returns null when the expression cannot be parsed, or when it matches nothing
|
|
59
|
+
* within roughly four years (`0 0 30 2 *` — 30 February). Callers must treat
|
|
60
|
+
* null as "do not schedule": inventing a fallback interval is how a job that
|
|
61
|
+
* was never meant to run ends up running daily.
|
|
62
|
+
*/
|
|
63
|
+
export declare function nextCronRun(expression: string, from?: Date, timeZone?: string): Date | null;
|
|
64
|
+
/** Milliseconds until the next run, or null when the expression never fires. */
|
|
65
|
+
export declare function msUntilNextCronRun(expression: string, from?: Date, timeZone?: string): number | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** biome-ignore-all lint/suspicious/noExplicitAny: <drizzle column builders are complex> */
|
|
2
2
|
import type { Algorithm, AuditSeverity } from 'src/Services';
|
|
3
|
-
export type SystemTableFeatureSets = 'authentication' | 'authorization' | 'audit' | 'backup' | 'notification' | 'verification' | 'multi-tenant' | 'payment' | 'storage' | 'chat' | 'monitoring' | 'domains' | 'secrets';
|
|
3
|
+
export type SystemTableFeatureSets = 'authentication' | 'authorization' | 'audit' | 'backup' | 'notification' | 'verification' | 'multi-tenant' | 'payment' | 'storage' | 'chat' | 'monitoring' | 'domains' | 'secrets' | 'integrations';
|
|
4
4
|
export type GenericNucleusMethods = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'TOGGLE' | 'VERIFICATION';
|
|
5
5
|
export type NucleusColumnType = 'integer' | 'smallint' | 'bigint' | 'serial' | 'smallserial' | 'bigserial' | 'real' | 'doublePrecision' | 'numeric' | 'decimal' | 'text' | 'varchar' | 'char' | 'uuid' | 'boolean' | 'date' | 'time' | 'timestamp' | 'timestamptz' | 'interval' | 'json' | 'jsonb' | 'bytea' | 'inet' | 'cidr' | 'macaddr' | 'macaddr8' | 'bit' | 'bitVarying' | 'point' | 'line' | 'lseg' | 'box' | 'path' | 'polygon' | 'circle' | 'tsvector' | 'tsquery' | 'xml' | 'money' | 'int4range' | 'int8range' | 'numrange' | 'tsrange' | 'tstzrange' | 'daterange' | 'int4multirange' | 'int8multirange' | 'nummultirange' | 'tsmultirange' | 'tstzmultirange' | 'datemultirange' | 'vector' | 'geometry' | 'geography' | 'customType';
|
|
6
6
|
export type NucleusFKAction = 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default';
|
|
@@ -691,6 +691,50 @@ export interface NucleusConfigOptions {
|
|
|
691
691
|
basePath?: string;
|
|
692
692
|
};
|
|
693
693
|
};
|
|
694
|
+
/**
|
|
695
|
+
* Pulls data from third-party HTTP APIs into this app's own entities.
|
|
696
|
+
*
|
|
697
|
+
* An operator registers a source (base URL + credentials), the endpoints on it,
|
|
698
|
+
* and a mapping from each endpoint's response onto a target entity. A run then
|
|
699
|
+
* fetches, maps, validates and writes — as ONE database transaction, so a run
|
|
700
|
+
* either lands completely or leaves nothing behind.
|
|
701
|
+
*
|
|
702
|
+
* The source's address carries no scheme requirement: `http://` to a service in
|
|
703
|
+
* the same cluster is as valid as `https://` to the public internet.
|
|
704
|
+
*/
|
|
705
|
+
integrations?: {
|
|
706
|
+
enabled?: boolean;
|
|
707
|
+
/** Route prefix for the run/test/preview endpoints. Default: `/integrations`. */
|
|
708
|
+
basePath?: string;
|
|
709
|
+
/**
|
|
710
|
+
* Roles allowed to configure sources and start runs, alongside godmin. A run
|
|
711
|
+
* writes to arbitrary entities, so this is effectively an administrative
|
|
712
|
+
* grant; empty means godmin only.
|
|
713
|
+
*/
|
|
714
|
+
adminRoles?: string[];
|
|
715
|
+
/**
|
|
716
|
+
* Ceiling on records held for one run. A run assembles everything before it
|
|
717
|
+
* writes anything — that is what makes it atomic — so the ceiling is real
|
|
718
|
+
* memory, and exceeding it fails the run instead of the process. Default: 50000.
|
|
719
|
+
*/
|
|
720
|
+
maxRecordsPerRun?: number;
|
|
721
|
+
/**
|
|
722
|
+
* Concurrent child requests when an endpoint fans out over another endpoint's
|
|
723
|
+
* records (one call per parent row). Default: 4 — deliberately low, because
|
|
724
|
+
* the far side is somebody else's production API.
|
|
725
|
+
*/
|
|
726
|
+
maxConcurrentFanout?: number;
|
|
727
|
+
/**
|
|
728
|
+
* How long a SUCCEEDED run stays revertible. Before-images are kept for this
|
|
729
|
+
* long; after it, the run reports that it can no longer be undone rather than
|
|
730
|
+
* offering a button that quietly does nothing. Default: `30d`.
|
|
731
|
+
*/
|
|
732
|
+
retainPreImagesFor?: string;
|
|
733
|
+
/** Fallback per-request timeout when a source doesn't set its own. Default: 30000. */
|
|
734
|
+
requestTimeoutMs?: number;
|
|
735
|
+
/** Prefix for the realtime topic a run publishes progress on. Default: `integrations`. */
|
|
736
|
+
realtimeTopicPrefix?: string;
|
|
737
|
+
};
|
|
694
738
|
notification?: {
|
|
695
739
|
enabled?: boolean;
|
|
696
740
|
channels?: {
|
|
@@ -949,8 +993,30 @@ export interface NucleusConfigOptions {
|
|
|
949
993
|
encryptionKey?: string;
|
|
950
994
|
schedule?: {
|
|
951
995
|
enabled?: boolean;
|
|
996
|
+
/**
|
|
997
|
+
* Standard five-field cron: `minute hour day-of-month month day-of-week`,
|
|
998
|
+
* with `*`, values, `a-b` ranges, `/step`, comma lists, three-letter names
|
|
999
|
+
* (`MON`, `JAN`) and the `@daily` family of macros.
|
|
1000
|
+
*
|
|
1001
|
+
* All five fields are honoured. They were not always: the previous
|
|
1002
|
+
* evaluator read only minute and hour, so `0 3 * * 0` — Sundays at 03:00 —
|
|
1003
|
+
* ran every night.
|
|
1004
|
+
*
|
|
1005
|
+
* An expression that cannot be read falls back to `0 2 * * *` and logs an
|
|
1006
|
+
* error. Refusing to schedule would be worse: the old evaluator ran
|
|
1007
|
+
* anything it did not understand every 24h, so a deployment with an exotic
|
|
1008
|
+
* expression was being backed up, and silently stopping that trades a
|
|
1009
|
+
* backup at the wrong hour for no backup at all.
|
|
1010
|
+
*/
|
|
952
1011
|
cron?: string;
|
|
953
1012
|
retentionDays?: number;
|
|
1013
|
+
/**
|
|
1014
|
+
* IANA time zone the cron expression is read in, e.g. `Europe/Istanbul`.
|
|
1015
|
+
* Defaults to UTC. Previously the container's own clock decided, which
|
|
1016
|
+
* meant the configured hour and the operator's hour were different numbers
|
|
1017
|
+
* with nothing saying so.
|
|
1018
|
+
*/
|
|
1019
|
+
timeZone?: string;
|
|
954
1020
|
};
|
|
955
1021
|
};
|
|
956
1022
|
entities: NucleusTable[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.783",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -1032,6 +1032,49 @@
|
|
|
1032
1032
|
}
|
|
1033
1033
|
}
|
|
1034
1034
|
},
|
|
1035
|
+
"integrations": {
|
|
1036
|
+
"type": "object",
|
|
1037
|
+
"description": "Pulls data from third-party HTTP APIs into this app's own entities. A run fetches, maps, validates and writes as ONE transaction.",
|
|
1038
|
+
"properties": {
|
|
1039
|
+
"enabled": {
|
|
1040
|
+
"type": "boolean"
|
|
1041
|
+
},
|
|
1042
|
+
"basePath": {
|
|
1043
|
+
"type": "string",
|
|
1044
|
+
"description": "Route prefix for run/test/preview endpoints. Default: /integrations"
|
|
1045
|
+
},
|
|
1046
|
+
"adminRoles": {
|
|
1047
|
+
"type": "array",
|
|
1048
|
+
"items": {
|
|
1049
|
+
"type": "string"
|
|
1050
|
+
},
|
|
1051
|
+
"description": "Roles allowed to configure sources and start runs, alongside godmin. Empty means godmin only."
|
|
1052
|
+
},
|
|
1053
|
+
"maxRecordsPerRun": {
|
|
1054
|
+
"type": "integer",
|
|
1055
|
+
"minimum": 1,
|
|
1056
|
+
"description": "Ceiling on records held for one run. Default: 50000"
|
|
1057
|
+
},
|
|
1058
|
+
"maxConcurrentFanout": {
|
|
1059
|
+
"type": "integer",
|
|
1060
|
+
"minimum": 1,
|
|
1061
|
+
"description": "Concurrent child requests when an endpoint fans out over another endpoint's records. Default: 4"
|
|
1062
|
+
},
|
|
1063
|
+
"retainPreImagesFor": {
|
|
1064
|
+
"type": "string",
|
|
1065
|
+
"description": "How long a succeeded run stays revertible, e.g. 30d. Default: 30d"
|
|
1066
|
+
},
|
|
1067
|
+
"requestTimeoutMs": {
|
|
1068
|
+
"type": "integer",
|
|
1069
|
+
"minimum": 1,
|
|
1070
|
+
"description": "Fallback per-request timeout when a source doesn't set its own. Default: 30000"
|
|
1071
|
+
},
|
|
1072
|
+
"realtimeTopicPrefix": {
|
|
1073
|
+
"type": "string",
|
|
1074
|
+
"description": "Prefix for the realtime topic a run publishes progress on. Default: integrations"
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
},
|
|
1035
1078
|
"notification": {
|
|
1036
1079
|
"type": "object",
|
|
1037
1080
|
"properties": {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { selectEmittedTables } from './generate-schema'
|
|
3
|
+
|
|
4
|
+
const table = (table_name: string) => ({ table_name })
|
|
5
|
+
|
|
6
|
+
const sets = (over: { system?: string[]; enabled?: string[]; custom?: string[] }) => ({
|
|
7
|
+
systemTableNames: new Set(over.system ?? []),
|
|
8
|
+
enabledTableNames: new Set(over.enabled ?? []),
|
|
9
|
+
customTableNames: new Set(over.custom ?? []),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('selectEmittedTables', () => {
|
|
13
|
+
it('emits a plain config entity', () => {
|
|
14
|
+
const out = selectEmittedTables([table('employees')], sets({ custom: ['employees'] }))
|
|
15
|
+
expect(out.map((t) => t.table_name)).toEqual(['employees'])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('emits a system table whose feature is on', () => {
|
|
19
|
+
const out = selectEmittedTables(
|
|
20
|
+
[table('verifications')],
|
|
21
|
+
sets({ system: ['verifications'], enabled: ['verifications'] })
|
|
22
|
+
)
|
|
23
|
+
expect(out).toHaveLength(1)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('drops a system table whose feature is off', () => {
|
|
27
|
+
const out = selectEmittedTables([table('verifications')], sets({ system: ['verifications'] }))
|
|
28
|
+
expect(out).toEqual([])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('a new built-in must not delete an app table of the same name', () => {
|
|
32
|
+
// The regression: nucleus shipped `integration_sources` as a system table.
|
|
33
|
+
// An app that already had its OWN table by that name, and has not turned the
|
|
34
|
+
// new feature on, saw it vanish from the generated schema — and on a
|
|
35
|
+
// deployment with allowDataLoss the next push DROPS it, with its contents.
|
|
36
|
+
it('keeps the app’s table when the new feature is off', () => {
|
|
37
|
+
const out = selectEmittedTables(
|
|
38
|
+
[table('integration_sources')],
|
|
39
|
+
sets({ system: ['integration_sources'], custom: ['integration_sources'] })
|
|
40
|
+
)
|
|
41
|
+
expect(out.map((t) => t.table_name)).toEqual(['integration_sources'])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('still keeps it when the feature is on — the app definition won the merge', () => {
|
|
45
|
+
const out = selectEmittedTables(
|
|
46
|
+
[table('integration_sources')],
|
|
47
|
+
sets({
|
|
48
|
+
system: ['integration_sources'],
|
|
49
|
+
enabled: ['integration_sources'],
|
|
50
|
+
custom: ['integration_sources'],
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
expect(out).toHaveLength(1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('does not resurrect a built-in the app never declared', () => {
|
|
57
|
+
// The other half: a table only nucleus knows about stays gated by its
|
|
58
|
+
// feature, so enabling nothing adds nothing.
|
|
59
|
+
const out = selectEmittedTables(
|
|
60
|
+
[table('integration_run_items')],
|
|
61
|
+
sets({ system: ['integration_run_items'], custom: ['integration_sources'] })
|
|
62
|
+
)
|
|
63
|
+
expect(out).toEqual([])
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('preserves order', () => {
|
|
68
|
+
const out = selectEmittedTables(
|
|
69
|
+
[table('a'), table('b'), table('c')],
|
|
70
|
+
sets({ custom: ['a', 'b', 'c'] })
|
|
71
|
+
)
|
|
72
|
+
expect(out.map((t) => t.table_name)).toEqual(['a', 'b', 'c'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('emits nothing from nothing', () => {
|
|
76
|
+
expect(selectEmittedTables([], sets({}))).toEqual([])
|
|
77
|
+
})
|
|
78
|
+
})
|