Line data Source code
1 : import * as dashql from '@ankoh/dashql-core';
2 : import type * as app_session from '@ankoh/dashql-jsonschema/app_session.js';
3 :
4 : import { CONNECTOR_INFOS, ConnectorType, ConnectorInfo } from './connector_info.js';
5 : import { ConnectionStateWithoutId, createConnectionState } from './connection_state.js';
6 : import { ConnectionStateDetailsVariant } from './connection_state_details.js';
7 : import { CONNECTOR_DESCRIPTORS, connectionParamsOf, connectionStateDetailsOf, connectorOfDetails, connectorOfParams, connectorParams } from './connector_descriptor.js';
8 :
9 : // Re-export connection param types from JSON Schema
10 : export type ConnectionParams = app_session.ConnectionParams;
11 : export type HyperConnectionParams = app_session.HyperConnectionParams;
12 : export type SalesforceConnectionParams = app_session.SalesforceConnectionParams;
13 : export type TrinoConnectionParams = app_session.TrinoConnectionParams;
14 :
15 : /// Raw form rows belong only to the local draft, never to committed/runtime parameters.
16 1 : export function connectionParamsWithoutDraftRows(params: ConnectionParams): ConnectionParams {
17 1 : const connector = connectorOfParams(params);
18 1 : return connectionParamsOf(connector, connector.withoutDraftRows(connectorParams(connector, params)));
19 1 : }
20 :
21 : /// Sort the keys of an object so that its encoding doesn't depend on their order
22 1 : function sortObjectKeys(value: unknown): unknown {
23 1 : return value != null && typeof value === 'object' && !Array.isArray(value)
24 1 : ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value;
25 1 : }
26 :
27 : /// Is a value an empty list or an empty map?
28 1 : function isEmptyCollection(value: unknown): boolean {
29 1 : return value != null && typeof value === 'object' && Object.keys(value).length === 0;
30 1 : }
31 :
32 : /// A canonical encoding of connection params that doesn't depend on the order of object keys
33 1 : export function connectionParamsKey(params: ConnectionParams | undefined): string {
34 1 : return JSON.stringify(params, (_key, value) => sortObjectKeys(value)) ?? '';
35 1 : }
36 :
37 1 : export function connectionParamsEqual(left: ConnectionParams | undefined, right: ConnectionParams | undefined): boolean {
38 1 : return connectionParamsKey(left) === connectionParamsKey(right);
39 1 : }
40 :
41 : /// A canonical encoding of the database that connection params describe.
42 : /// Secrets and login hints are stripped like for sharing, and empty lists and maps count as absent:
43 : /// default params leave them out, params written by a form or a session file carry them empty.
44 1 : function connectionIdentityKey(params: ConnectionParams): string {
45 1 : return JSON.stringify(sanitizeConnectionParamsForSharing(params, false), function (this: unknown, _key, value) {
46 1 : return !Array.isArray(this) && isEmptyCollection(value) ? undefined : sortObjectKeys(value);
47 1 : }) ?? '';
48 1 : }
49 :
50 : /// Do two params describe the same connection, ignoring everything that sharing strips?
51 : /// Shared sessions carry sanitized params without secrets and login hints, they still describe the
52 : /// connection a local session uses with the full params.
53 1 : export function connectionParamsShareEqual(left: ConnectionParams, right: ConnectionParams): boolean {
54 1 : return connectionIdentityKey(left) === connectionIdentityKey(right);
55 1 : }
56 :
57 : /// Do the relations of a connection outlive the app? See `ConnectorDescriptor.catalogIsDurable`.
58 1 : export function connectionCatalogIsDurable(params: ConnectionParams): boolean {
59 1 : const connector = connectorOfParams(params);
60 1 : return connector.catalogIsDurable(connectorParams(connector, params));
61 1 : }
62 :
63 1 : export function getConnectionStateDetailsFromParams(params: ConnectionParams): ConnectionStateDetailsVariant {
64 1 : const connector = connectorOfParams(params);
65 1 : return connectionStateDetailsOf(connector, connectorParams(connector, params));
66 1 : }
67 :
68 1 : export function getConnectionParamsFromStateDetails(details: ConnectionStateDetailsVariant): ConnectionParams | null {
69 1 : const setupParams = details.value.proto.setupParams;
70 1 : return setupParams ? connectionParamsOf(connectorOfDetails(details), setupParams) : null;
71 1 : }
72 :
73 : /// Strip sensitive credentials from connection params before they are encoded into a shared
74 : /// link or file. Identity/hint fields (instance urls, consumer keys, usernames, login hints)
75 : /// are preserved so a recipient can be prompted with a prefilled sign-in, but no secret that
76 : /// would grant access on its own is included. This is always applied when connection info is
77 : /// shared — sharing raw secrets is never intended.
78 : ///
79 : /// The login hint (the resolved account username / login) identifies the sharer personally, so
80 : /// it is only carried when `withLoginHint` is true. When false it is cleared while the rest of
81 : /// the (non-secret) connection identity is preserved.
82 1 : export function sanitizeConnectionParamsForSharing(params: ConnectionParams, withLoginHint: boolean = true): ConnectionParams {
83 1 : const connector = connectorOfParams(params);
84 1 : const withoutDraftRows = connector.withoutDraftRows(connectorParams(connector, params));
85 1 : return connectionParamsOf(connector, connector.sanitizeForSharing(withoutDraftRows, withLoginHint));
86 1 : }
87 :
88 : /// Whether the connection params carry a login hint (the resolved account username / login) that
89 : /// could be shared. Used to decide if the "share login hint" toggle should be offered at all.
90 1 : export function connectionParamsHaveLoginHint(params: ConnectionParams | null): boolean {
91 1 : if (!params) return false;
92 1 : const connector = connectorOfParams(params);
93 1 : return connector.loginHint(connectorParams(connector, params)) != null;
94 1 : }
95 :
96 : /// The params that identify the results of a query in the query result cache
97 1 : export function createConnectionParamsSignature(params: ConnectionParams): unknown {
98 1 : const connector = connectorOfParams(params);
99 1 : return connector.cacheSignature(connectorParams(connector, params));
100 1 : }
101 :
102 1 : export function createConnectionStateFromParams(dql: dashql.DashQL, params: ConnectionParams): ConnectionStateWithoutId {
103 1 : return createConnectionState(dql, connectorOfParams(params).info, getConnectionStateDetailsFromParams(params), { params });
104 1 : }
105 :
106 1 : export function createDefaultHyperWasmConnectionState(dql: dashql.DashQL): ConnectionStateWithoutId {
107 1 : const connector = CONNECTOR_INFOS[ConnectorType.HYPER];
108 1 : return createConnectionStateFromParams(dql, createDefaultConnectionParamsForConnector(connector));
109 1 : }
110 :
111 1 : export function createDefaultConnectionParamsForConnector(info: ConnectorInfo): ConnectionParams {
112 1 : const connector = CONNECTOR_DESCRIPTORS[info.connectorType];
113 1 : return connectionParamsOf(connector, connector.defaultParams());
114 1 : }
115 :
116 : /// The endpoint that a new connection to a Hyper server starts with
117 1 : export const DEFAULT_HYPER_ENDPOINT = 'http://localhost:7484';
118 :
119 : /// The params of a new connection that the user attaches.
120 : /// The app holds a single connection to the embedded Hyper, an attached Hyper connection speaks to a server.
121 1 : export function createAttachedConnectionParamsForConnector(info: ConnectorInfo): ConnectionParams {
122 1 : const params = createDefaultConnectionParamsForConnector(info);
123 1 : if (info.connectorType !== ConnectorType.HYPER || !('hyper' in params)) return params;
124 1 : return { ...params, hyper: { ...params.hyper, protocol: 'V3_HTTP', endpoint: DEFAULT_HYPER_ENDPOINT } };
125 1 : }
|