nucleus-core-ts 0.9.821 → 0.9.823

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/client.js +1 -1
  2. package/dist/fe/components/IntegrationsPage/components/ConnectionList.js +5 -0
  3. package/dist/fe/components/IntegrationsPage/components/ConnectionTransfer.d.ts +7 -0
  4. package/dist/fe/components/IntegrationsPage/components/ConnectionTransfer.js +157 -0
  5. package/dist/fe/components/IntegrationsPage/components/MappingAccountFields.d.ts +12 -0
  6. package/dist/fe/components/IntegrationsPage/components/MappingAccountFields.js +214 -0
  7. package/dist/fe/components/IntegrationsPage/components/MappingEditor.js +10 -0
  8. package/dist/fe/components/IntegrationsPage/components/RunPanel.js +14 -3
  9. package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.d.ts +15 -0
  10. package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.js +120 -0
  11. package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.test.d.ts +1 -0
  12. package/dist/fe/components/IntegrationsPage/components/RunPlanSuggestions.test.js +103 -0
  13. package/dist/fe/components/IntegrationsPage/components/RunPlanSummary.js +5 -0
  14. package/dist/fe/components/IntegrationsPage/components/fieldMappingOptions.js +4 -2
  15. package/dist/fe/components/IntegrationsPage/components/mappingDraft.d.ts +3 -1
  16. package/dist/fe/components/IntegrationsPage/components/mappingDraft.js +14 -0
  17. package/dist/fe/components/IntegrationsPage/index.d.ts +3 -0
  18. package/dist/fe/components/IntegrationsPage/index.js +3 -0
  19. package/dist/fe/components/IntegrationsPage/types/actions.d.ts +18 -0
  20. package/dist/fe/components/IntegrationsPage/types/records.d.ts +26 -1
  21. package/dist/fe/components/IntegrationsPage/types/results.d.ts +2 -0
  22. package/dist/index.js +5 -5
  23. package/dist/src/ElysiaPlugin/routes/integrations/inspect.d.ts +1 -0
  24. package/dist/src/ElysiaPlugin/routes/integrations/repository.d.ts +36 -0
  25. package/dist/src/Services/Integrations/plan.d.ts +18 -1
  26. package/dist/src/Services/Integrations/runner.d.ts +13 -1
  27. package/dist/src/Services/Integrations/transfer.d.ts +123 -0
  28. package/dist/src/Services/Integrations/transfer.test.d.ts +1 -0
  29. package/dist/src/Services/Integrations/types.d.ts +47 -2
  30. package/dist/src/Services/Integrations/writer.d.ts +24 -0
  31. package/package.json +1 -1
  32. package/src/system.tables.json +12 -0
@@ -81,6 +81,7 @@ export declare function previewRun(chain: ResolvedChain, tokenCache: TokenCache,
81
81
  readEntityColumn: (entity: string, field: string) => Promise<unknown[]>;
82
82
  requiredFields: (entity: string) => string[];
83
83
  uniqueFields: (entity: string) => string[];
84
+ readEntityIdsBy: (entity: string, field: string) => Promise<Record<string, string>>;
84
85
  }, deps?: {
85
86
  fetchImpl?: typeof fetch;
86
87
  maxRecords?: number;
@@ -12,6 +12,7 @@ import type { EndpointSpec } from 'src/Services/Integrations/collect';
12
12
  import type { SourceConnection } from 'src/Services/Integrations/fetcher';
13
13
  import type { OAuthConfig } from 'src/Services/Integrations/oauth';
14
14
  import type { MappingSpec, RunRepository } from 'src/Services/Integrations/runner';
15
+ import type { TransferDocument } from 'src/Services/Integrations/transfer';
15
16
  import type { PreImage } from 'src/Services/Integrations/writer';
16
17
  import type { Logger } from 'src/Services/Logger';
17
18
  import type { NucleusTable } from 'src/types';
@@ -75,6 +76,41 @@ export declare function resolveEndpointContext(deps: RepositoryDeps, by: {
75
76
  */
76
77
  export declare function cacheEndpointSample(deps: RepositoryDeps, endpointId: string, records: readonly unknown[]): Promise<void>;
77
78
  /** Columns of a target that must carry a value, for pre-write validation. */
79
+ /**
80
+ * Everything one connection is made of, for an export.
81
+ *
82
+ * Read as three plain selects rather than through `resolveChain`, which starts
83
+ * from a mapping — a connection worth exporting may have endpoints and no
84
+ * mappings yet, and that is a legitimate thing to move.
85
+ */
86
+ export declare function readConnectionBundle(deps: RepositoryDeps, sourceId: string): Promise<{
87
+ source: Record<string, unknown>;
88
+ endpoints: Record<string, unknown>[];
89
+ mappings: Record<string, unknown>[];
90
+ } | null>;
91
+ /**
92
+ * Writes an exported connection into this installation.
93
+ *
94
+ * Matched by NAME throughout — the connection by its slug, endpoints and
95
+ * mappings by their name — so bringing the same file in twice updates what is
96
+ * here instead of leaving two of everything. That is the behaviour somebody
97
+ * actually wants: the file is how a configuration is kept in step between two
98
+ * environments, not a one-shot.
99
+ *
100
+ * Credentials are neither in the file nor invented here. A connection arrives
101
+ * unable to authenticate, and says so, rather than arriving with an empty token
102
+ * that looks configured.
103
+ */
104
+ export declare function applyTransferDocument(deps: RepositoryDeps, document: TransferDocument, userId: string): Promise<{
105
+ ok: true;
106
+ sourceId: string;
107
+ created: boolean;
108
+ endpoints: number;
109
+ mappings: number;
110
+ } | {
111
+ ok: false;
112
+ error: string;
113
+ }>;
78
114
  export declare function requiredFieldsOf(entities: NucleusTable[], targetEntity: string): string[];
79
115
  /**
80
116
  * Columns the target enforces as unique, single-column only.
@@ -11,7 +11,7 @@
11
11
  * second implementation that drifts. And every rule about destroying data is
12
12
  * testable here without a database.
13
13
  */
14
- import type { MappedRecord, MissingRecordPolicy, ReferenceCheck, ReferenceInsert, RunIssue, RunPlan, WriteMode } from './types';
14
+ import type { AccountInsert, AccountRule, MappedRecord, MissingRecordPolicy, ReferenceCheck, ReferenceInsert, RunIssue, RunPlan, WriteMode } from './types';
15
15
  /** A row already in the target table. */
16
16
  export type ExistingRow = {
17
17
  id: string;
@@ -35,6 +35,13 @@ export type WriteOperations = {
35
35
  * them. Empty unless a reference is set to `create`.
36
36
  */
37
37
  referenceInserts: ReferenceInsert[];
38
+ /**
39
+ * Accounts a run would create so its records have a login.
40
+ *
41
+ * Written before the records that point at them, in the same transaction: the
42
+ * person and their account both exist, or neither does.
43
+ */
44
+ accountInserts: AccountInsert[];
38
45
  updates: UpdateOp[];
39
46
  /** Rows to remove — from `replace`, or from the `delete` missing policy. */
40
47
  deletes: ExistingRow[];
@@ -76,6 +83,16 @@ export type PlanInput = {
76
83
  * required field, because the column is filled — just not by the source.
77
84
  */
78
85
  createDefaults?: Readonly<Record<string, unknown>>;
86
+ /** Creates an account alongside each new record, and links it. */
87
+ accountRule?: AccountRule | null;
88
+ /**
89
+ * Accounts that already exist, keyed by their match value.
90
+ *
91
+ * Loaded by the caller, like reference values: a record whose e-mail already
92
+ * has an account is LINKED to it rather than given a second one, which is what
93
+ * makes re-running an import safe.
94
+ */
95
+ existingAccounts?: Readonly<Record<string, string>>;
79
96
  /** Columns whose value has to already exist in another table. */
80
97
  references?: readonly ReferenceCheck[];
81
98
  /**
@@ -24,7 +24,7 @@ import { type EndpointSpec } from './collect';
24
24
  import type { FetchImpl, SourceConnection } from './fetcher';
25
25
  import { type OAuthConfig, type TokenCache } from './oauth';
26
26
  import { type ExistingRow } from './plan';
27
- import type { FieldMapping, MissingRecordPolicy, ReferenceCheck, RunFailure, RunIssue, RunPlan, RunProgress, WriteMode } from './types';
27
+ import type { AccountRule, FieldMapping, MissingRecordPolicy, ReferenceCheck, RunFailure, RunIssue, RunPlan, RunProgress, WriteMode } from './types';
28
28
  import { type PreImage, type WriterDeps } from './writer';
29
29
  /** The mapping being run, resolved from the database by the caller. */
30
30
  export type MappingSpec = {
@@ -47,6 +47,8 @@ export type MappingSpec = {
47
47
  * accounts must not also leave the key to them lying in a config table.
48
48
  */
49
49
  passwordColumn?: string | null;
50
+ /** Creates a login alongside each new record, in the same transaction. */
51
+ accountRule?: AccountRule | null;
50
52
  };
51
53
  /** Everything a run needs, already read and decrypted. */
52
54
  export type RunContext = {
@@ -67,6 +69,12 @@ export type RunContext = {
67
69
  * handles a plaintext secret, and never persisted: it belongs to this run.
68
70
  */
69
71
  createDefaults?: Record<string, unknown>;
72
+ /**
73
+ * The shared opening password, already hashed.
74
+ *
75
+ * Prepared by the caller so nothing in the engine handles a plaintext secret.
76
+ */
77
+ accountPasswordHash?: string | null;
70
78
  };
71
79
  /** Persistence and lookups the runner needs. Injected so it can be tested. */
72
80
  export type RunRepository = {
@@ -96,6 +104,8 @@ export type RunRepository = {
96
104
  readExistingRows(targetEntity: string): Promise<ExistingRow[]>;
97
105
  /** Distinct values of one column, for an `entity` parameter binding. */
98
106
  readEntityColumn(entity: string, field: string): Promise<unknown[]>;
107
+ /** `value → id` for one column, so an existing account can be linked rather than doubled. */
108
+ readEntityIdsBy(entity: string, field: string): Promise<Record<string, string>>;
99
109
  /** NOT NULL, no-default columns of the target. */
100
110
  requiredFields(targetEntity: string): string[];
101
111
  /** Single-column unique constraints of the target. */
@@ -128,6 +138,8 @@ export type RunOutcome = {
128
138
  failure: RunFailure | null;
129
139
  plan: RunPlan | null;
130
140
  created: number;
141
+ /** Accounts created so this run's records have a login. */
142
+ accountsCreated: number;
131
143
  /** Rows added to OTHER tables so this run's records could point at them. */
132
144
  referencesCreated: number;
133
145
  updated: number;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Moving a whole integration between installations.
3
+ *
4
+ * A connection is not one record. It is a connection, the calls it makes, the
5
+ * parameters those calls take from each other, and the mappings that turn the
6
+ * answers into rows — and rebuilding that by hand in a second environment is
7
+ * both hours of work and a fresh chance to get one field wrong in a way nobody
8
+ * notices until an import writes it.
9
+ *
10
+ * So it travels as one document.
11
+ *
12
+ * ## What deliberately does NOT travel
13
+ *
14
+ * Every secret. Tokens, client secrets, OAuth passwords: none of them are in the
15
+ * document, and the import asks for them again on the other side. A file that
16
+ * carries credentials is a file that gets mailed, pasted into a ticket and
17
+ * committed — and the whole point of storing them encrypted is undone by one
18
+ * convenient export.
19
+ *
20
+ * ## Why names, not identifiers
21
+ *
22
+ * An endpoint's parameters can be bound to another endpoint, and a mapping reads
23
+ * from one. Those are foreign keys, and a foreign key means nothing in a
24
+ * database that has never seen it. On the way out they become the endpoint's
25
+ * NAME; on the way in they are looked up again. So a document stays valid across
26
+ * installations, and a re-import onto an existing connection updates what is
27
+ * there rather than duplicating it.
28
+ */
29
+ import type { FieldMapping, MissingRecordPolicy, ReferenceCheck, WriteMode } from './types';
30
+ /** Bumped only when an older document could be READ wrongly, not merely partially. */
31
+ export declare const TRANSFER_VERSION = 1;
32
+ export type TransferEndpoint = {
33
+ /** Unique within the document; how bindings and mappings refer to it. */
34
+ name: string;
35
+ method: string;
36
+ path: string;
37
+ description?: string | null;
38
+ queryParams?: Record<string, unknown> | null;
39
+ requestBody?: Record<string, unknown> | null;
40
+ responseRootPath?: string | null;
41
+ pagination?: Record<string, unknown> | null;
42
+ /** `endpointId` is replaced by `endpointName`; every other form is unchanged. */
43
+ paramBindings?: Record<string, unknown>[] | null;
44
+ enabled?: boolean;
45
+ };
46
+ export type TransferMapping = {
47
+ name: string;
48
+ /** The endpoint this mapping reads, by name. */
49
+ endpointName: string | null;
50
+ targetEntity: string;
51
+ fieldMappings: FieldMapping[];
52
+ referenceChecks?: ReferenceCheck[];
53
+ dedupSourceField?: string | null;
54
+ dedupTargetField?: string | null;
55
+ writeMode: WriteMode;
56
+ missingRecordPolicy: MissingRecordPolicy;
57
+ missingRecordColumn?: string | null;
58
+ missingRecordValue?: string | null;
59
+ passwordTargetColumn?: string | null;
60
+ enabled?: boolean;
61
+ };
62
+ export type TransferDocument = {
63
+ version: number;
64
+ /** Stamped by the caller; the engine has no clock of its own to trust. */
65
+ exportedAt?: string;
66
+ connection: {
67
+ name: string;
68
+ slug: string;
69
+ baseUrl: string;
70
+ description?: string | null;
71
+ authType: string;
72
+ authHeaderName?: string | null;
73
+ authQueryParam?: string | null;
74
+ extraHeaders?: Record<string, string> | null;
75
+ tokenUrl?: string | null;
76
+ oauthGrantType?: string | null;
77
+ oauthClientId?: string | null;
78
+ oauthUsername?: string | null;
79
+ oauthScope?: string | null;
80
+ oauthClientAuth?: string | null;
81
+ tlsVerify?: boolean;
82
+ timeoutMs?: number | null;
83
+ enabled?: boolean;
84
+ };
85
+ endpoints: TransferEndpoint[];
86
+ mappings: TransferMapping[];
87
+ /**
88
+ * Named so the person opening the file knows what it cannot do on its own.
89
+ * Read by nothing; it exists to be read by a human.
90
+ */
91
+ secretsExcluded: true;
92
+ };
93
+ /** True when a row carries something that must not be exported. Used by tests. */
94
+ export declare function hasSecret(row: Record<string, unknown>): boolean;
95
+ /**
96
+ * Builds the document from rows as they are stored.
97
+ *
98
+ * Field by field rather than by spreading the row: a column added to the table
99
+ * later — including the next encrypted one — must not ride out to a file simply
100
+ * because nobody updated this function.
101
+ */
102
+ export declare function buildTransferDocument(input: {
103
+ source: Record<string, unknown>;
104
+ endpoints: Record<string, unknown>[];
105
+ mappings: Record<string, unknown>[];
106
+ exportedAt?: string;
107
+ }): TransferDocument;
108
+ export type TransferProblem = {
109
+ path: string;
110
+ message: string;
111
+ };
112
+ /**
113
+ * Checks a document before anything is written.
114
+ *
115
+ * Refusing a bad file costs one message; accepting one costs a half-built
116
+ * connection that looks configured and imports nothing — or worse, imports into
117
+ * the wrong table.
118
+ */
119
+ export declare function validateTransferDocument(value: unknown): {
120
+ ok: boolean;
121
+ document: TransferDocument | null;
122
+ problems: TransferProblem[];
123
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -11,7 +11,9 @@ export type MappingTransform = 'none' | 'trim' | 'upper' | 'lower' | 'toString'
11
11
  /** Folds accents, lowercases, and removes what an address cannot contain. */
12
12
  | 'emailSafe'
13
13
  /** The last part of a path-like value: `A/B/C` → `C`. */
14
- | 'pathLeaf';
14
+ | 'pathLeaf'
15
+ /** Wraps a value as a list, for a column that holds one: `100712` → `['100712']`. */
16
+ | 'toList';
15
17
  /** One field of a mapping: external `source` path → internal `target` column. */
16
18
  export type FieldMapping = {
17
19
  /** Dot-path into the external record, e.g. `employeeNo` or `department.code`. */
@@ -133,7 +135,7 @@ export type RunStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancell
133
135
  */
134
136
  export type RunIssue = {
135
137
  sourceIndex: number;
136
- kind: 'missing_required' | 'missing_dedup_key' | 'duplicate_in_batch' | 'duplicate_value' | 'invalid_value' | 'unmapped' | 'missing_reference';
138
+ kind: 'missing_required' | 'missing_dedup_key' | 'duplicate_in_batch' | 'duplicate_value' | 'invalid_value' | 'unmapped' | 'missing_reference' | 'account_unmatched';
137
139
  message: string;
138
140
  dedupKey?: string | null;
139
141
  /**
@@ -189,6 +191,47 @@ export type ReferenceCheck = {
189
191
  entityColumn: string;
190
192
  onMissing: ReferenceOnMissing;
191
193
  };
194
+ /**
195
+ * An account created alongside each imported record.
196
+ *
197
+ * A directory carries people, not logins. Importing eight hundred employees and
198
+ * then creating eight hundred accounts by hand — matching them up by e-mail, one
199
+ * at a time — is the job nobody does, so the import that was supposed to save the
200
+ * afternoon produces a table of people who cannot sign in.
201
+ *
202
+ * So the account is part of the same import and the same transaction: either the
203
+ * person and their login both exist, or neither does.
204
+ *
205
+ * The password is NOT here. It belongs to the run — asked for once, used for
206
+ * every account that run creates, and stored nowhere. A password on a mapping is
207
+ * a password that outlives the reason for it.
208
+ */
209
+ export type AccountRule = {
210
+ /** Entity the account row goes into, e.g. `users`. */
211
+ entity: string;
212
+ /** Column on the TARGET entity that receives the account's id, e.g. `userId`. */
213
+ idColumn: string;
214
+ /** Column on the account entity that identifies it, e.g. `email`. */
215
+ matchColumn: string;
216
+ /** Column of the MAPPED record whose value is matched against it. */
217
+ matchFrom: string;
218
+ /** Column on the account entity that receives the run's shared password. */
219
+ passwordColumn: string;
220
+ /** Other columns to fill on a new account, taken from the mapped record. */
221
+ values?: {
222
+ column: string;
223
+ from: string;
224
+ }[];
225
+ };
226
+ /** An account a run would create so an imported record has a login. */
227
+ export type AccountInsert = {
228
+ entity: string;
229
+ /** What identifies it, so a row can be matched back to the record that needs it. */
230
+ matchColumn: string;
231
+ matchValue: string;
232
+ /** Everything to write, except the password the writer adds. */
233
+ data: Record<string, unknown>;
234
+ };
192
235
  /** A value a run would add to a referenced table so a record can point at it. */
193
236
  export type ReferenceInsert = {
194
237
  entity: string;
@@ -251,6 +294,8 @@ export type RunPlan = {
251
294
  * approving, not read about afterwards.
252
295
  */
253
296
  referenceInserts: ReferenceInsert[];
297
+ /** Accounts this run would create so its records have a login. */
298
+ accountsToCreate: number;
254
299
  /** A few mapped rows so an operator can eyeball the mapping before committing. */
255
300
  sample: Record<string, unknown>[];
256
301
  };
@@ -80,6 +80,28 @@ export type ApplyInput = {
80
80
  ops: WriteOperations;
81
81
  targetEntity: string;
82
82
  userId: string;
83
+ /**
84
+ * How an account is created alongside each record, and where its id goes.
85
+ *
86
+ * The password arrives already hashed: nothing in the engine handles a
87
+ * plaintext secret, and it is never written anywhere but the account row.
88
+ */
89
+ account?: {
90
+ idColumn: string;
91
+ /** Column on the ACCOUNT row that identifies it. */
92
+ matchColumn: string;
93
+ /**
94
+ * Column of the RECORD carrying the same value.
95
+ *
96
+ * Not the same thing as `matchColumn`, even when they share a name: one
97
+ * names a column on `users`, the other a column on `employees`, and reading
98
+ * the record with the account's column name is how a rename in either table
99
+ * would start handing people each other's logins.
100
+ */
101
+ matchFrom: string;
102
+ passwordColumn: string;
103
+ passwordHash: string;
104
+ } | null;
83
105
  /** Ownership predicates from the caller's claims. ANDed onto updates/deletes. */
84
106
  scopeConditions?: SQL[];
85
107
  /** Column and value written to rows the source no longer carries. */
@@ -91,6 +113,8 @@ export type ApplyInput = {
91
113
  export type ApplyResult = {
92
114
  ok: true;
93
115
  created: number;
116
+ /** Accounts created so this run's records have a login. */
117
+ accountsCreated: number;
94
118
  /** Rows added to OTHER tables so this run's records could point at them. */
95
119
  referencesCreated: number;
96
120
  updated: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nucleus-core-ts",
3
- "version": "0.9.821",
3
+ "version": "0.9.823",
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",
@@ -5331,6 +5331,11 @@
5331
5331
  "length": 100,
5332
5332
  "comment": "Column that receives the shared opening password given at run time. The password itself is never stored."
5333
5333
  },
5334
+ {
5335
+ "name": "account_rule",
5336
+ "type": "jsonb",
5337
+ "comment": "Creates a login alongside each imported record, in the same transaction, and links it. The password belongs to the run, never to this row."
5338
+ },
5334
5339
  { "name": "schedule_cron", "type": "varchar", "length": 120 },
5335
5340
  {
5336
5341
  "name": "schedule_timezone",
@@ -5411,6 +5416,13 @@
5411
5416
  "default": 0,
5412
5417
  "comment": "Rows this run added to OTHER tables so its records could point at them."
5413
5418
  },
5419
+ {
5420
+ "name": "accounts_created_count",
5421
+ "type": "integer",
5422
+ "notNull": true,
5423
+ "default": 0,
5424
+ "comment": "Logins this run created so its records could be signed in to."
5425
+ },
5414
5426
  { "name": "updated_count", "type": "integer", "notNull": true, "default": 0 },
5415
5427
  { "name": "skipped_count", "type": "integer", "notNull": true, "default": 0 },
5416
5428
  { "name": "deleted_count", "type": "integer", "notNull": true, "default": 0 },