@loykin/datasourcekit 0.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LeeSuk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,330 @@
1
+ # @loykin/datasourcekit
2
+
3
+ DatasourceKit is a frontend contract layer for building datasource management and query experiences in dashboard, query editor, and reporting tools.
4
+
5
+ DatasourceKit is not a backend, not a datasource store, and not the source of truth for permissions or secrets. Datasource types, datasource instances, secrets, authorization, and actual query execution belong to your application backend.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @loykin/datasourcekit
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ This quickstart shows the complete flow:
16
+
17
+ 1. Expose an application backend contract.
18
+ 2. Define a datasource plugin.
19
+ 3. Create a manager.
20
+ 4. Register a datasource instance.
21
+ 5. Run a query through the backend.
22
+ 6. Normalize the backend raw response with the plugin transform.
23
+
24
+ ### 1. Expose Backend APIs
25
+
26
+ Your backend can be written in Go, Java, Python, Node, or anything else. DatasourceKit does not run in your backend. It only needs the frontend to call backend APIs with this shape.
27
+
28
+ | Operation | Example endpoint | Backend responsibility |
29
+ |---|---|---|
30
+ | List types | `GET /api/datasource-types` | Return installed/available datasource types |
31
+ | Get type | `GET /api/datasource-types/:type` | Return one datasource type |
32
+ | List instances | `GET /api/datasources` | Return datasource instances visible to the user |
33
+ | Create instance | `POST /api/datasources` | Validate, authorize, store safe config, create uid |
34
+ | Update instance | `PATCH /api/datasources/:uid` | Validate, authorize, handle version/conflict |
35
+ | Delete instance | `DELETE /api/datasources/:uid` | Authorize and delete |
36
+ | Query | `POST /api/datasource-query` | Load instance, load secrets, authorize, execute driver |
37
+ | Health | `GET /api/datasources/:uid/health` | Check connection/status |
38
+
39
+ Example query request body:
40
+
41
+ ```json
42
+ {
43
+ "id": "q1",
44
+ "datasourceUid": "postgres-main",
45
+ "datasourceType": "postgres",
46
+ "query": {
47
+ "rawSql": "select * from users limit 10"
48
+ },
49
+ "options": {
50
+ "timeoutMs": 30000
51
+ }
52
+ }
53
+ ```
54
+
55
+ The backend should return either a normalized `QueryResult` or a type-specific raw response that the plugin can normalize.
56
+
57
+ ### 2. Create A Frontend Backend Client
58
+
59
+ This is frontend code. It can call any backend implementation.
60
+
61
+ ```ts
62
+ const backend = {
63
+ listDatasourceTypes: (ctx) =>
64
+ api.get('/api/datasource-types', ctx),
65
+ getDatasourceType: (type, ctx) =>
66
+ api.get(`/api/datasource-types/${type}`, ctx),
67
+ listDatasourceInstances: (options, ctx) =>
68
+ api.get('/api/datasources', { ...ctx, params: options }),
69
+ getDatasourceInstance: (uid, ctx) =>
70
+ api.get(`/api/datasources/${uid}`, ctx),
71
+ createDatasourceInstance: (input, ctx) =>
72
+ api.post('/api/datasources', input, ctx),
73
+ updateDatasourceInstance: (uid, patch, ctx) =>
74
+ api.patch(`/api/datasources/${uid}`, patch, ctx),
75
+ deleteDatasourceInstance: (uid, ctx) =>
76
+ api.delete(`/api/datasources/${uid}`, ctx),
77
+ queryDatasource: (request, ctx) =>
78
+ api.post('/api/datasource-query', request, ctx),
79
+ }
80
+ ```
81
+
82
+ ### 3. Define A Datasource Plugin
83
+
84
+ A plugin owns the type-specific UI hooks and response normalization. PostgreSQL and ClickHouse may return different raw backend response shapes, so `transform` belongs to the plugin, not to the manager backend.
85
+
86
+ ```ts
87
+ import {
88
+ createDatasourceManager,
89
+ defineDatasourcePlugin,
90
+ type QueryResult,
91
+ } from '@loykin/datasourcekit'
92
+
93
+ type PostgresOptions = {
94
+ host: string
95
+ port: number
96
+ database: string
97
+ }
98
+
99
+ type PostgresQuery = {
100
+ rawSql: string
101
+ }
102
+
103
+ function normalizePostgresResult(raw: unknown): QueryResult {
104
+ const response = raw as {
105
+ fields: string[]
106
+ rows: unknown[][]
107
+ requestId: string
108
+ }
109
+
110
+ return {
111
+ columns: response.fields.map((name) => ({ name, type: 'string' })),
112
+ rows: response.rows,
113
+ requestId: response.requestId,
114
+ }
115
+ }
116
+
117
+ const postgresPlugin = defineDatasourcePlugin<PostgresOptions, PostgresQuery>({
118
+ type: 'postgres',
119
+ name: 'PostgreSQL',
120
+ configEditor: (props) => PostgresConfigEditor(props),
121
+ queryEditor: (props) => PostgresQueryEditor(props),
122
+ backend: {
123
+ transform: (raw, request) => {
124
+ // request.query is PostgresQuery in this plugin.
125
+ return normalizePostgresResult(raw)
126
+ },
127
+ },
128
+ })
129
+ ```
130
+
131
+ ### 4. Create The Manager
132
+
133
+ `createDatasourceManager` wires frontend plugin routing to your backend handlers.
134
+
135
+ ```ts
136
+ const manager = createDatasourceManager({
137
+ plugins: [postgresPlugin],
138
+ backend: {
139
+ types: {
140
+ list: (ctx) => backend.listDatasourceTypes(ctx),
141
+ get: (type, ctx) => backend.getDatasourceType(type, ctx),
142
+ },
143
+ instances: {
144
+ list: (options, ctx) => backend.listDatasourceInstances(options, ctx),
145
+ get: (uid, ctx) => backend.getDatasourceInstance(uid, ctx),
146
+ create: (input, ctx) => backend.createDatasourceInstance(input, ctx),
147
+ update: (uid, patch, ctx) => backend.updateDatasourceInstance(uid, patch, ctx),
148
+ delete: (uid, ctx) => backend.deleteDatasourceInstance(uid, ctx),
149
+ },
150
+ query: (request, ctx) => backend.queryDatasource(request, ctx),
151
+ },
152
+ })
153
+ ```
154
+
155
+ ### 5. Register A Datasource Instance
156
+
157
+ `options` is type-specific safe config. Do not put passwords, tokens, or other secrets here. Secrets should stay in the backend.
158
+
159
+ ```ts
160
+ const datasource = await manager.instances.create({
161
+ type: 'postgres',
162
+ name: 'Main PostgreSQL',
163
+ options: {
164
+ host: 'localhost',
165
+ port: 5432,
166
+ database: 'app',
167
+ },
168
+ })
169
+ ```
170
+
171
+ ### 6. Run A Query
172
+
173
+ `query` is the type-specific query body. `options` is execution metadata such as timeout, max rows, or cache hints.
174
+
175
+ ```ts
176
+ const result = await manager.instances.query({
177
+ id: 'q1',
178
+ datasourceUid: datasource.uid,
179
+ datasourceType: 'postgres',
180
+ query: {
181
+ rawSql: 'select * from users limit 10',
182
+ },
183
+ options: {
184
+ timeoutMs: 30000,
185
+ },
186
+ })
187
+ ```
188
+
189
+ Execution flow:
190
+
191
+ ```txt
192
+ manager.instances.query(request)
193
+ -> registry.get('postgres')
194
+ -> backend.query(request)
195
+ -> postgresPlugin.backend.transform(raw, request)
196
+ -> QueryResult
197
+ ```
198
+
199
+ ## Manager API
200
+
201
+ ```ts
202
+ const types = await manager.types.list(ctx)
203
+ const type = await manager.types.get('postgres', ctx)
204
+
205
+ const { items } = await manager.instances.list({ filter: { type: 'postgres' } }, ctx)
206
+ const datasource = await manager.instances.get('postgres-main', ctx)
207
+ await manager.instances.update(datasource.uid, { name: 'Renamed' }, ctx)
208
+ await manager.instances.delete(datasource.uid, ctx)
209
+ ```
210
+
211
+ Optional type management handlers can be exposed when your backend supports them:
212
+
213
+ ```ts
214
+ await manager.types.install?.('postgres', ctx)
215
+ await manager.types.uninstall?.('postgres', ctx)
216
+ await manager.types.enable?.('postgres', ctx)
217
+ await manager.types.disable?.('postgres', ctx)
218
+ ```
219
+
220
+ ## Type-specific Options And Query
221
+
222
+ DatasourceKit core does not enforce datasource-specific config or query shapes. `TOptions` and `TQuery` are defined by each plugin.
223
+
224
+ ```ts
225
+ type ClickHouseOptions = {
226
+ host: string
227
+ port: number
228
+ database: string
229
+ }
230
+
231
+ type ClickHouseQuery = {
232
+ rawSql: string
233
+ format?: 'JSONEachRow' | 'TabSeparated'
234
+ }
235
+
236
+ const clickhousePlugin = defineDatasourcePlugin<ClickHouseOptions, ClickHouseQuery>({
237
+ type: 'clickhouse',
238
+ name: 'ClickHouse',
239
+ queryEditor: ({ query, onChange, onRunQuery }) =>
240
+ ClickHouseQueryEditor({ query, onChange, onRunQuery }),
241
+ backend: {
242
+ transform: (raw, request) => {
243
+ // request.query is ClickHouseQuery in this plugin.
244
+ return normalizeClickHouseResult(raw)
245
+ },
246
+ },
247
+ })
248
+ ```
249
+
250
+ Examples:
251
+
252
+ | Type | `query` example |
253
+ |---|---|
254
+ | PostgreSQL | `{ rawSql: 'select * from users' }` |
255
+ | ClickHouse | `{ rawSql: 'select count() from events', format: 'JSONEachRow' }` |
256
+ | Prometheus | `{ promql: 'rate(http_requests_total[5m])', step: '30s' }` |
257
+ | Redis | `{ command: 'INFO' }` |
258
+
259
+ ## Capabilities
260
+
261
+ Query execution is separate from datasource capabilities. Capabilities are helper operations for management screens and query editors.
262
+
263
+ ```ts
264
+ const health = await manager.instances.healthCheck(uid, type, ctx)
265
+ const namespaces = await manager.instances.listNamespaces(uid, type, ctx)
266
+ const fields = await manager.instances.listFields(uid, type, { namespaceId }, ctx)
267
+ ```
268
+
269
+ These calls may be implemented by `plugin.backend.*` or by the manager backend fallback.
270
+
271
+ ## Permissions
272
+
273
+ The backend is the final authority for permissions. DatasourceKit only passes request context and exposes backend-provided permission hints.
274
+
275
+ Frontend permission hints are for UI behavior only, such as hiding or disabling buttons. Every `create`, `update`, `delete`, and `query` call must still be checked by the backend.
276
+
277
+ ## REST Helper
278
+
279
+ If your backend follows the helper's REST convention, you can use `createRestDatasourceManager` as a backend adapter. If your API paths, auth scheme, or error envelope are different, customize the helper or wire handlers directly with `createDatasourceManager`.
280
+
281
+ ```ts
282
+ import {
283
+ createDatasourceManager,
284
+ createRestDatasourceManager,
285
+ } from '@loykin/datasourcekit'
286
+
287
+ const manager = createDatasourceManager({
288
+ plugins: [postgresPlugin],
289
+ backend: createRestDatasourceManager({
290
+ baseUrl: 'https://api.example.com/datasources',
291
+ getHeaders: () => ({ authorization: `Bearer ${token}` }),
292
+ }),
293
+ })
294
+ ```
295
+
296
+ Custom paths and response envelopes:
297
+
298
+ ```ts
299
+ const backend = createRestDatasourceManager({
300
+ baseUrl: 'https://api.example.com/v1',
301
+ paths: {
302
+ typesList: () => '/catalog/datasource-types',
303
+ typeGet: (type) => `/catalog/datasource-types/${type}`,
304
+ instancesList: (queryString) => `/connections${queryString}`,
305
+ query: () => '/query/run',
306
+ },
307
+ unwrap: (body) => body.data,
308
+ createError: (response, body) => {
309
+ if (response.status === 500) {
310
+ return new DatasourceTransportError(body.error?.message, response.status)
311
+ }
312
+ return undefined
313
+ },
314
+ })
315
+ ```
316
+
317
+ ## Error Model
318
+
319
+ Backend failures and stale state are surfaced as datasource domain errors.
320
+
321
+ | Error | When it happens |
322
+ |---|---|
323
+ | `DatasourceTypeNotRegisteredError` | The requested datasource type has no registered frontend plugin |
324
+ | `DatasourceNotFoundError` | The datasource uid or type no longer exists |
325
+ | `DatasourceForbiddenError` | The backend rejected the action due to permissions |
326
+ | `DatasourceUnauthorizedError` | The request is not authenticated |
327
+ | `DatasourceConflictError` | A stale update/delete was rejected |
328
+ | `DatasourceValidationError` | Datasource config or query input is invalid |
329
+ | `DatasourceTransportError` | Network or backend transport failed |
330
+ | `DatasourceCapabilityError` | The plugin/backend does not support the requested capability |
@@ -0,0 +1,55 @@
1
+ // src/errors.ts
2
+ var DatasourceNotFoundError = class extends Error {
3
+ constructor(uid) {
4
+ super(`datasource "${uid}" not found`);
5
+ this.name = "DatasourceNotFoundError";
6
+ }
7
+ };
8
+ var DatasourceCapabilityError = class extends Error {
9
+ constructor(uid, capability) {
10
+ super(`datasource "${uid}" does not support ${capability}`);
11
+ this.name = "DatasourceCapabilityError";
12
+ }
13
+ };
14
+ var DatasourceTypeNotRegisteredError = class extends Error {
15
+ constructor(type) {
16
+ super(`datasource type "${type}" is not registered`);
17
+ this.name = "DatasourceTypeNotRegisteredError";
18
+ }
19
+ };
20
+ var DatasourceUnauthorizedError = class extends Error {
21
+ constructor(message = "datasource request is not authenticated") {
22
+ super(message);
23
+ this.name = "DatasourceUnauthorizedError";
24
+ }
25
+ };
26
+ var DatasourceForbiddenError = class extends Error {
27
+ constructor(message = "datasource request is not allowed") {
28
+ super(message);
29
+ this.name = "DatasourceForbiddenError";
30
+ }
31
+ };
32
+ var DatasourceConflictError = class extends Error {
33
+ constructor(message = "datasource was modified by another actor") {
34
+ super(message);
35
+ this.name = "DatasourceConflictError";
36
+ }
37
+ };
38
+ var DatasourceValidationError = class extends Error {
39
+ constructor(message = "datasource validation failed", errors) {
40
+ super(message);
41
+ this.errors = errors;
42
+ this.name = "DatasourceValidationError";
43
+ }
44
+ };
45
+ var DatasourceTransportError = class extends Error {
46
+ constructor(message = "datasource backend request failed", status) {
47
+ super(message);
48
+ this.status = status;
49
+ this.name = "DatasourceTransportError";
50
+ }
51
+ };
52
+
53
+ export { DatasourceCapabilityError, DatasourceConflictError, DatasourceForbiddenError, DatasourceNotFoundError, DatasourceTransportError, DatasourceTypeNotRegisteredError, DatasourceUnauthorizedError, DatasourceValidationError };
54
+ //# sourceMappingURL=chunk-Z2DGIUJ2.js.map
55
+ //# sourceMappingURL=chunk-Z2DGIUJ2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts"],"names":[],"mappings":";AAAO,IAAM,uBAAA,GAAN,cAAsC,KAAA,CAAM;AAAA,EACjD,YAAY,GAAA,EAAa;AACvB,IAAA,KAAA,CAAM,CAAA,YAAA,EAAe,GAAG,CAAA,WAAA,CAAa,CAAA;AACrC,IAAA,IAAA,CAAK,IAAA,GAAO,yBAAA;AAAA,EACd;AACF;AAEO,IAAM,yBAAA,GAAN,cAAwC,KAAA,CAAM;AAAA,EACnD,WAAA,CAAY,KAAa,UAAA,EAAoB;AAC3C,IAAA,KAAA,CAAM,CAAA,YAAA,EAAe,GAAG,CAAA,mBAAA,EAAsB,UAAU,CAAA,CAAE,CAAA;AAC1D,IAAA,IAAA,CAAK,IAAA,GAAO,2BAAA;AAAA,EACd;AACF;AAEO,IAAM,gCAAA,GAAN,cAA+C,KAAA,CAAM;AAAA,EAC1D,YAAY,IAAA,EAAc;AACxB,IAAA,KAAA,CAAM,CAAA,iBAAA,EAAoB,IAAI,CAAA,mBAAA,CAAqB,CAAA;AACnD,IAAA,IAAA,CAAK,IAAA,GAAO,kCAAA;AAAA,EACd;AACF;AAEO,IAAM,2BAAA,GAAN,cAA0C,KAAA,CAAM;AAAA,EACrD,WAAA,CAAY,UAAU,yCAAA,EAA2C;AAC/D,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,6BAAA;AAAA,EACd;AACF;AAEO,IAAM,wBAAA,GAAN,cAAuC,KAAA,CAAM;AAAA,EAClD,WAAA,CAAY,UAAU,mCAAA,EAAqC;AACzD,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,0BAAA;AAAA,EACd;AACF;AAEO,IAAM,uBAAA,GAAN,cAAsC,KAAA,CAAM;AAAA,EACjD,WAAA,CAAY,UAAU,0CAAA,EAA4C;AAChE,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,yBAAA;AAAA,EACd;AACF;AAEO,IAAM,yBAAA,GAAN,cAAwC,KAAA,CAAM;AAAA,EACnD,WAAA,CACE,OAAA,GAAU,8BAAA,EACD,MAAA,EACT;AACA,IAAA,KAAA,CAAM,OAAO,CAAA;AAFJ,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAGT,IAAA,IAAA,CAAK,IAAA,GAAO,2BAAA;AAAA,EACd;AACF;AAEO,IAAM,wBAAA,GAAN,cAAuC,KAAA,CAAM;AAAA,EAClD,WAAA,CACE,OAAA,GAAU,mCAAA,EACD,MAAA,EACT;AACA,IAAA,KAAA,CAAM,OAAO,CAAA;AAFJ,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAGT,IAAA,IAAA,CAAK,IAAA,GAAO,0BAAA;AAAA,EACd;AACF","file":"chunk-Z2DGIUJ2.js","sourcesContent":["export class DatasourceNotFoundError extends Error {\n constructor(uid: string) {\n super(`datasource \"${uid}\" not found`)\n this.name = 'DatasourceNotFoundError'\n }\n}\n\nexport class DatasourceCapabilityError extends Error {\n constructor(uid: string, capability: string) {\n super(`datasource \"${uid}\" does not support ${capability}`)\n this.name = 'DatasourceCapabilityError'\n }\n}\n\nexport class DatasourceTypeNotRegisteredError extends Error {\n constructor(type: string) {\n super(`datasource type \"${type}\" is not registered`)\n this.name = 'DatasourceTypeNotRegisteredError'\n }\n}\n\nexport class DatasourceUnauthorizedError extends Error {\n constructor(message = 'datasource request is not authenticated') {\n super(message)\n this.name = 'DatasourceUnauthorizedError'\n }\n}\n\nexport class DatasourceForbiddenError extends Error {\n constructor(message = 'datasource request is not allowed') {\n super(message)\n this.name = 'DatasourceForbiddenError'\n }\n}\n\nexport class DatasourceConflictError extends Error {\n constructor(message = 'datasource was modified by another actor') {\n super(message)\n this.name = 'DatasourceConflictError'\n }\n}\n\nexport class DatasourceValidationError extends Error {\n constructor(\n message = 'datasource validation failed',\n readonly errors?: string[],\n ) {\n super(message)\n this.name = 'DatasourceValidationError'\n }\n}\n\nexport class DatasourceTransportError extends Error {\n constructor(\n message = 'datasource backend request failed',\n readonly status?: number,\n ) {\n super(message)\n this.name = 'DatasourceTransportError'\n }\n}\n"]}