@stardeck-customer-apps/data-store-sdk 0.1.0-preview.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/dist/chunk-7YN3WP4H.mjs +43 -0
- package/dist/cli/generate-types.d.mts +1 -0
- package/dist/cli/generate-types.d.ts +1 -0
- package/dist/cli/generate-types.js +174 -0
- package/dist/cli/generate-types.mjs +151 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +73 -0
- package/dist/index.mjs +14 -0
- package/dist/server/index.d.mts +108 -0
- package/dist/server/index.d.ts +108 -0
- package/dist/server/index.js +265 -0
- package/dist/server/index.mjs +213 -0
- package/dist/types-CqVD-TLQ.d.mts +68 -0
- package/dist/types-CqVD-TLQ.d.ts +68 -0
- package/package.json +65 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { D as DataStoreClientConfig, a as TableSchema, C as ColumnDefinition, b as QueryOptions, c as QueryResult, L as ListObjectsResult } from '../types-CqVD-TLQ.js';
|
|
2
|
+
import { KyselyConfig, Kysely } from 'kysely';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Data Store client for server-side use in deployed projects.
|
|
6
|
+
*
|
|
7
|
+
* Provides three capabilities:
|
|
8
|
+
* - Schema management (create/alter tables and columns)
|
|
9
|
+
* - Dynamic queries (read/write data without generated types)
|
|
10
|
+
* - Storage operations (upload/download/list files for storage-type stores)
|
|
11
|
+
*
|
|
12
|
+
* All operations go through the Stardeck platform API with HMAC authentication.
|
|
13
|
+
* For type-safe queries, use Kysely with a direct connection string instead.
|
|
14
|
+
*/
|
|
15
|
+
declare class DataStoreClient {
|
|
16
|
+
private baseUrl;
|
|
17
|
+
private storeId;
|
|
18
|
+
private deploymentSecret;
|
|
19
|
+
private organizationId;
|
|
20
|
+
private projectId;
|
|
21
|
+
private deploymentId;
|
|
22
|
+
private maxRetries;
|
|
23
|
+
private debug;
|
|
24
|
+
constructor(config: DataStoreClientConfig);
|
|
25
|
+
private getEnv;
|
|
26
|
+
private log;
|
|
27
|
+
private getAuthHeader;
|
|
28
|
+
private request;
|
|
29
|
+
getSchema(): Promise<{
|
|
30
|
+
tables: TableSchema[];
|
|
31
|
+
}>;
|
|
32
|
+
createTable(name: string, columns: ColumnDefinition[]): Promise<{
|
|
33
|
+
tableName: string;
|
|
34
|
+
}>;
|
|
35
|
+
addColumn(tableName: string, column: ColumnDefinition): Promise<{
|
|
36
|
+
columnName: string;
|
|
37
|
+
}>;
|
|
38
|
+
updateColumn(tableName: string, columnName: string, changes: {
|
|
39
|
+
newName?: string;
|
|
40
|
+
newType?: string;
|
|
41
|
+
currentType?: string;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
success: boolean;
|
|
44
|
+
}>;
|
|
45
|
+
deleteColumn(tableName: string, columnName: string): Promise<{
|
|
46
|
+
deletedAs: string;
|
|
47
|
+
}>;
|
|
48
|
+
query(tableName: string, options?: QueryOptions): Promise<QueryResult>;
|
|
49
|
+
insert(tableName: string, row: Record<string, unknown>): Promise<{
|
|
50
|
+
row: Record<string, unknown>;
|
|
51
|
+
}>;
|
|
52
|
+
update(tableName: string, primaryKey: Record<string, unknown>, column: string, value: unknown): Promise<{
|
|
53
|
+
row: Record<string, unknown>;
|
|
54
|
+
}>;
|
|
55
|
+
delete(tableName: string, primaryKey: Record<string, unknown>): Promise<{
|
|
56
|
+
deleted: boolean;
|
|
57
|
+
}>;
|
|
58
|
+
listFiles(options?: {
|
|
59
|
+
prefix?: string;
|
|
60
|
+
cursor?: string;
|
|
61
|
+
}): Promise<ListObjectsResult>;
|
|
62
|
+
getUploadUrl(key: string, contentType: string, size: number): Promise<{
|
|
63
|
+
uploadUrl: string;
|
|
64
|
+
key: string;
|
|
65
|
+
}>;
|
|
66
|
+
getDownloadUrl(key: string): Promise<{
|
|
67
|
+
downloadUrl: string;
|
|
68
|
+
}>;
|
|
69
|
+
deleteFiles(keys: string[]): Promise<{
|
|
70
|
+
deleted: number;
|
|
71
|
+
}>;
|
|
72
|
+
createFolder(path: string): Promise<{
|
|
73
|
+
key: string;
|
|
74
|
+
}>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
declare function signDeploymentRequest(deploymentSecret: string, payload: {
|
|
78
|
+
organizationId: string;
|
|
79
|
+
projectId: string;
|
|
80
|
+
deploymentId: string;
|
|
81
|
+
}): string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Creates a Kysely instance for direct database access to a data store.
|
|
85
|
+
* Use this for type-safe queries when you have generated types and a connection string.
|
|
86
|
+
*
|
|
87
|
+
* Requires `@neondatabase/serverless` and `kysely-neon` as peer dependencies.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* import { createDataStore } from "@stardeck-customer-apps/data-store-sdk/server";
|
|
92
|
+
* import type { DB } from "./generated/data-store-types";
|
|
93
|
+
*
|
|
94
|
+
* const db = await createDataStore<DB>();
|
|
95
|
+
*
|
|
96
|
+
* const users = await db
|
|
97
|
+
* .selectFrom("users")
|
|
98
|
+
* .where("status", "=", "active")
|
|
99
|
+
* .selectAll()
|
|
100
|
+
* .execute();
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
declare function createDataStore<DB>(options?: {
|
|
104
|
+
connectionString?: string;
|
|
105
|
+
kyselyConfig?: Partial<KyselyConfig>;
|
|
106
|
+
}): Promise<Kysely<DB>>;
|
|
107
|
+
|
|
108
|
+
export { DataStoreClient, createDataStore, signDeploymentRequest };
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/server/index.ts
|
|
31
|
+
var server_exports = {};
|
|
32
|
+
__export(server_exports, {
|
|
33
|
+
DataStoreClient: () => DataStoreClient,
|
|
34
|
+
createDataStore: () => createDataStore,
|
|
35
|
+
signDeploymentRequest: () => signDeploymentRequest
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(server_exports);
|
|
38
|
+
|
|
39
|
+
// src/errors.ts
|
|
40
|
+
var DataStoreError = class extends Error {
|
|
41
|
+
code;
|
|
42
|
+
statusCode;
|
|
43
|
+
constructor(message, code, statusCode) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "DataStoreError";
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.statusCode = statusCode;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var AuthenticationError = class extends DataStoreError {
|
|
51
|
+
constructor(message = "Authentication failed") {
|
|
52
|
+
super(message, "AUTHENTICATION_ERROR", 401);
|
|
53
|
+
this.name = "AuthenticationError";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/server/hmac.ts
|
|
58
|
+
var import_crypto = __toESM(require("crypto"));
|
|
59
|
+
function signDeploymentRequest(deploymentSecret, payload) {
|
|
60
|
+
const requestPayload = {
|
|
61
|
+
type: "deployment-request",
|
|
62
|
+
organizationId: payload.organizationId,
|
|
63
|
+
projectId: payload.projectId,
|
|
64
|
+
deploymentId: payload.deploymentId,
|
|
65
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
66
|
+
nonce: import_crypto.default.randomUUID()
|
|
67
|
+
};
|
|
68
|
+
const payloadJson = JSON.stringify(requestPayload);
|
|
69
|
+
const payloadB64 = Buffer.from(payloadJson).toString("base64");
|
|
70
|
+
const signature = import_crypto.default.createHmac("sha256", deploymentSecret).update(payloadJson).digest("hex");
|
|
71
|
+
return `${payloadB64}.${signature}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/server/client.ts
|
|
75
|
+
var DataStoreClient = class {
|
|
76
|
+
baseUrl;
|
|
77
|
+
storeId;
|
|
78
|
+
deploymentSecret;
|
|
79
|
+
organizationId;
|
|
80
|
+
projectId;
|
|
81
|
+
deploymentId;
|
|
82
|
+
maxRetries;
|
|
83
|
+
debug;
|
|
84
|
+
constructor(config) {
|
|
85
|
+
this.storeId = config.storeId;
|
|
86
|
+
this.baseUrl = config.baseUrl || this.getEnv("CONTROL_PLANE_URL") || "";
|
|
87
|
+
this.deploymentSecret = config.deploymentSecret || this.getEnv("DEPLOYMENT_SECRET") || "";
|
|
88
|
+
this.organizationId = config.organizationId || this.getEnv("ORGANIZATION_ID") || "";
|
|
89
|
+
this.projectId = config.projectId || this.getEnv("PROJECT_ID") || "";
|
|
90
|
+
this.deploymentId = config.deploymentId || this.getEnv("DEPLOYMENT_ID") || "";
|
|
91
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
92
|
+
this.debug = config.debug ?? false;
|
|
93
|
+
if (!this.baseUrl || !this.deploymentSecret || !this.organizationId || !this.projectId || !this.deploymentId) {
|
|
94
|
+
console.warn(
|
|
95
|
+
"[DataStoreClient] Not fully configured. Set CONTROL_PLANE_URL, DEPLOYMENT_SECRET, ORGANIZATION_ID, PROJECT_ID, and DEPLOYMENT_ID environment variables."
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
getEnv(key) {
|
|
100
|
+
if (typeof process !== "undefined" && process.env?.[key]) {
|
|
101
|
+
return process.env[key];
|
|
102
|
+
}
|
|
103
|
+
return void 0;
|
|
104
|
+
}
|
|
105
|
+
log(...args) {
|
|
106
|
+
if (this.debug) {
|
|
107
|
+
console.log("[DataStoreClient]", ...args);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
getAuthHeader() {
|
|
111
|
+
return signDeploymentRequest(this.deploymentSecret, {
|
|
112
|
+
organizationId: this.organizationId,
|
|
113
|
+
projectId: this.projectId,
|
|
114
|
+
deploymentId: this.deploymentId
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async request(method, path, body, queryParams) {
|
|
118
|
+
let url = `${this.baseUrl}/api/data-stores/${this.storeId}${path}`;
|
|
119
|
+
if (queryParams) {
|
|
120
|
+
const params = new URLSearchParams(queryParams);
|
|
121
|
+
if (method === "GET") {
|
|
122
|
+
params.set("deploymentId", this.deploymentId);
|
|
123
|
+
}
|
|
124
|
+
url += `?${params.toString()}`;
|
|
125
|
+
} else if (method === "GET") {
|
|
126
|
+
url += `?deploymentId=${this.deploymentId}`;
|
|
127
|
+
}
|
|
128
|
+
const requestBody = method !== "GET" && body ? { ...body, deploymentId: this.deploymentId } : method !== "GET" ? { deploymentId: this.deploymentId } : void 0;
|
|
129
|
+
let lastError = null;
|
|
130
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
131
|
+
try {
|
|
132
|
+
this.log(`${method} ${path}`, { attempt });
|
|
133
|
+
const response = await fetch(url, {
|
|
134
|
+
method,
|
|
135
|
+
headers: {
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
"X-Stardeck-Auth": this.getAuthHeader()
|
|
138
|
+
},
|
|
139
|
+
body: requestBody ? JSON.stringify(requestBody) : void 0
|
|
140
|
+
});
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const errorData = await response.json().catch(() => ({}));
|
|
143
|
+
const message = errorData.error || `HTTP ${response.status}`;
|
|
144
|
+
if (response.status === 401) {
|
|
145
|
+
throw new AuthenticationError(message);
|
|
146
|
+
}
|
|
147
|
+
throw new DataStoreError(message, "API_ERROR", response.status);
|
|
148
|
+
}
|
|
149
|
+
const data = await response.json();
|
|
150
|
+
if (data && typeof data === "object" && "success" in data && data.success && "data" in data) {
|
|
151
|
+
return data.data;
|
|
152
|
+
}
|
|
153
|
+
return data;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
156
|
+
if (error instanceof AuthenticationError) throw error;
|
|
157
|
+
if (error instanceof DataStoreError && error.statusCode < 500) throw error;
|
|
158
|
+
if (attempt < this.maxRetries) {
|
|
159
|
+
const delay = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
160
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw lastError ?? new DataStoreError("Request failed", "UNKNOWN", 500);
|
|
165
|
+
}
|
|
166
|
+
// ─── Schema Operations ──────────────────────────────────────
|
|
167
|
+
async getSchema() {
|
|
168
|
+
return this.request("GET", "/schema");
|
|
169
|
+
}
|
|
170
|
+
async createTable(name, columns) {
|
|
171
|
+
return this.request("POST", "/schema/tables", { name, columns });
|
|
172
|
+
}
|
|
173
|
+
async addColumn(tableName, column) {
|
|
174
|
+
return this.request("POST", "/schema/columns", { tableName, column });
|
|
175
|
+
}
|
|
176
|
+
async updateColumn(tableName, columnName, changes) {
|
|
177
|
+
return this.request("PATCH", "/schema/columns", {
|
|
178
|
+
tableName,
|
|
179
|
+
columnName,
|
|
180
|
+
...changes
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async deleteColumn(tableName, columnName) {
|
|
184
|
+
return this.request("DELETE", "/schema/columns", { tableName, columnName });
|
|
185
|
+
}
|
|
186
|
+
// ─── Query Operations ───────────────────────────────────────
|
|
187
|
+
async query(tableName, options = {}) {
|
|
188
|
+
return this.request("POST", "/query", {
|
|
189
|
+
tableName,
|
|
190
|
+
limit: options.limit ?? 50,
|
|
191
|
+
offset: options.offset ?? 0,
|
|
192
|
+
orderBy: options.orderBy,
|
|
193
|
+
orderDir: options.orderDir,
|
|
194
|
+
filters: options.filters ?? []
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async insert(tableName, row) {
|
|
198
|
+
return this.request("POST", "/mutate", {
|
|
199
|
+
tableName,
|
|
200
|
+
operation: "insert",
|
|
201
|
+
row
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async update(tableName, primaryKey, column, value) {
|
|
205
|
+
return this.request("POST", "/mutate", {
|
|
206
|
+
tableName,
|
|
207
|
+
operation: "update",
|
|
208
|
+
primaryKey,
|
|
209
|
+
column,
|
|
210
|
+
value
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async delete(tableName, primaryKey) {
|
|
214
|
+
return this.request("POST", "/mutate", {
|
|
215
|
+
tableName,
|
|
216
|
+
operation: "delete",
|
|
217
|
+
primaryKey
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// ─── Storage Operations ─────────────────────────────────────
|
|
221
|
+
async listFiles(options) {
|
|
222
|
+
const params = {};
|
|
223
|
+
if (options?.prefix) params.prefix = options.prefix;
|
|
224
|
+
if (options?.cursor) params.cursor = options.cursor;
|
|
225
|
+
return this.request("GET", "/storage", void 0, params);
|
|
226
|
+
}
|
|
227
|
+
async getUploadUrl(key, contentType, size) {
|
|
228
|
+
return this.request("POST", "/storage/upload", { key, contentType, size });
|
|
229
|
+
}
|
|
230
|
+
async getDownloadUrl(key) {
|
|
231
|
+
return this.request("GET", "/storage/download", void 0, { key });
|
|
232
|
+
}
|
|
233
|
+
async deleteFiles(keys) {
|
|
234
|
+
return this.request("DELETE", "/storage/objects", { keys });
|
|
235
|
+
}
|
|
236
|
+
async createFolder(path) {
|
|
237
|
+
return this.request("POST", "/storage/folder", { path });
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// src/server/kysely.ts
|
|
242
|
+
var import_kysely = require("kysely");
|
|
243
|
+
async function createDataStore(options) {
|
|
244
|
+
const connectionString = options?.connectionString ?? (typeof process !== "undefined" ? process.env?.DATA_STORE_URL : void 0);
|
|
245
|
+
if (!connectionString) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
"DATA_STORE_URL environment variable is required, or pass connectionString in options"
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const { Kysely: KyselyClass } = await import("kysely");
|
|
251
|
+
const { NeonDialect } = await import("kysely-neon");
|
|
252
|
+
const { neon } = await import("@neondatabase/serverless");
|
|
253
|
+
const sql = neon(connectionString);
|
|
254
|
+
const dialect = new NeonDialect({ neon: sql });
|
|
255
|
+
return new KyselyClass({
|
|
256
|
+
dialect,
|
|
257
|
+
...options?.kyselyConfig
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
261
|
+
0 && (module.exports = {
|
|
262
|
+
DataStoreClient,
|
|
263
|
+
createDataStore,
|
|
264
|
+
signDeploymentRequest
|
|
265
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthenticationError,
|
|
3
|
+
DataStoreError
|
|
4
|
+
} from "../chunk-7YN3WP4H.mjs";
|
|
5
|
+
|
|
6
|
+
// src/server/hmac.ts
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
function signDeploymentRequest(deploymentSecret, payload) {
|
|
9
|
+
const requestPayload = {
|
|
10
|
+
type: "deployment-request",
|
|
11
|
+
organizationId: payload.organizationId,
|
|
12
|
+
projectId: payload.projectId,
|
|
13
|
+
deploymentId: payload.deploymentId,
|
|
14
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
15
|
+
nonce: crypto.randomUUID()
|
|
16
|
+
};
|
|
17
|
+
const payloadJson = JSON.stringify(requestPayload);
|
|
18
|
+
const payloadB64 = Buffer.from(payloadJson).toString("base64");
|
|
19
|
+
const signature = crypto.createHmac("sha256", deploymentSecret).update(payloadJson).digest("hex");
|
|
20
|
+
return `${payloadB64}.${signature}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/server/client.ts
|
|
24
|
+
var DataStoreClient = class {
|
|
25
|
+
baseUrl;
|
|
26
|
+
storeId;
|
|
27
|
+
deploymentSecret;
|
|
28
|
+
organizationId;
|
|
29
|
+
projectId;
|
|
30
|
+
deploymentId;
|
|
31
|
+
maxRetries;
|
|
32
|
+
debug;
|
|
33
|
+
constructor(config) {
|
|
34
|
+
this.storeId = config.storeId;
|
|
35
|
+
this.baseUrl = config.baseUrl || this.getEnv("CONTROL_PLANE_URL") || "";
|
|
36
|
+
this.deploymentSecret = config.deploymentSecret || this.getEnv("DEPLOYMENT_SECRET") || "";
|
|
37
|
+
this.organizationId = config.organizationId || this.getEnv("ORGANIZATION_ID") || "";
|
|
38
|
+
this.projectId = config.projectId || this.getEnv("PROJECT_ID") || "";
|
|
39
|
+
this.deploymentId = config.deploymentId || this.getEnv("DEPLOYMENT_ID") || "";
|
|
40
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
41
|
+
this.debug = config.debug ?? false;
|
|
42
|
+
if (!this.baseUrl || !this.deploymentSecret || !this.organizationId || !this.projectId || !this.deploymentId) {
|
|
43
|
+
console.warn(
|
|
44
|
+
"[DataStoreClient] Not fully configured. Set CONTROL_PLANE_URL, DEPLOYMENT_SECRET, ORGANIZATION_ID, PROJECT_ID, and DEPLOYMENT_ID environment variables."
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
getEnv(key) {
|
|
49
|
+
if (typeof process !== "undefined" && process.env?.[key]) {
|
|
50
|
+
return process.env[key];
|
|
51
|
+
}
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
log(...args) {
|
|
55
|
+
if (this.debug) {
|
|
56
|
+
console.log("[DataStoreClient]", ...args);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
getAuthHeader() {
|
|
60
|
+
return signDeploymentRequest(this.deploymentSecret, {
|
|
61
|
+
organizationId: this.organizationId,
|
|
62
|
+
projectId: this.projectId,
|
|
63
|
+
deploymentId: this.deploymentId
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async request(method, path, body, queryParams) {
|
|
67
|
+
let url = `${this.baseUrl}/api/data-stores/${this.storeId}${path}`;
|
|
68
|
+
if (queryParams) {
|
|
69
|
+
const params = new URLSearchParams(queryParams);
|
|
70
|
+
if (method === "GET") {
|
|
71
|
+
params.set("deploymentId", this.deploymentId);
|
|
72
|
+
}
|
|
73
|
+
url += `?${params.toString()}`;
|
|
74
|
+
} else if (method === "GET") {
|
|
75
|
+
url += `?deploymentId=${this.deploymentId}`;
|
|
76
|
+
}
|
|
77
|
+
const requestBody = method !== "GET" && body ? { ...body, deploymentId: this.deploymentId } : method !== "GET" ? { deploymentId: this.deploymentId } : void 0;
|
|
78
|
+
let lastError = null;
|
|
79
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
80
|
+
try {
|
|
81
|
+
this.log(`${method} ${path}`, { attempt });
|
|
82
|
+
const response = await fetch(url, {
|
|
83
|
+
method,
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
"X-Stardeck-Auth": this.getAuthHeader()
|
|
87
|
+
},
|
|
88
|
+
body: requestBody ? JSON.stringify(requestBody) : void 0
|
|
89
|
+
});
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
const errorData = await response.json().catch(() => ({}));
|
|
92
|
+
const message = errorData.error || `HTTP ${response.status}`;
|
|
93
|
+
if (response.status === 401) {
|
|
94
|
+
throw new AuthenticationError(message);
|
|
95
|
+
}
|
|
96
|
+
throw new DataStoreError(message, "API_ERROR", response.status);
|
|
97
|
+
}
|
|
98
|
+
const data = await response.json();
|
|
99
|
+
if (data && typeof data === "object" && "success" in data && data.success && "data" in data) {
|
|
100
|
+
return data.data;
|
|
101
|
+
}
|
|
102
|
+
return data;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
105
|
+
if (error instanceof AuthenticationError) throw error;
|
|
106
|
+
if (error instanceof DataStoreError && error.statusCode < 500) throw error;
|
|
107
|
+
if (attempt < this.maxRetries) {
|
|
108
|
+
const delay = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
109
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
throw lastError ?? new DataStoreError("Request failed", "UNKNOWN", 500);
|
|
114
|
+
}
|
|
115
|
+
// ─── Schema Operations ──────────────────────────────────────
|
|
116
|
+
async getSchema() {
|
|
117
|
+
return this.request("GET", "/schema");
|
|
118
|
+
}
|
|
119
|
+
async createTable(name, columns) {
|
|
120
|
+
return this.request("POST", "/schema/tables", { name, columns });
|
|
121
|
+
}
|
|
122
|
+
async addColumn(tableName, column) {
|
|
123
|
+
return this.request("POST", "/schema/columns", { tableName, column });
|
|
124
|
+
}
|
|
125
|
+
async updateColumn(tableName, columnName, changes) {
|
|
126
|
+
return this.request("PATCH", "/schema/columns", {
|
|
127
|
+
tableName,
|
|
128
|
+
columnName,
|
|
129
|
+
...changes
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async deleteColumn(tableName, columnName) {
|
|
133
|
+
return this.request("DELETE", "/schema/columns", { tableName, columnName });
|
|
134
|
+
}
|
|
135
|
+
// ─── Query Operations ───────────────────────────────────────
|
|
136
|
+
async query(tableName, options = {}) {
|
|
137
|
+
return this.request("POST", "/query", {
|
|
138
|
+
tableName,
|
|
139
|
+
limit: options.limit ?? 50,
|
|
140
|
+
offset: options.offset ?? 0,
|
|
141
|
+
orderBy: options.orderBy,
|
|
142
|
+
orderDir: options.orderDir,
|
|
143
|
+
filters: options.filters ?? []
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async insert(tableName, row) {
|
|
147
|
+
return this.request("POST", "/mutate", {
|
|
148
|
+
tableName,
|
|
149
|
+
operation: "insert",
|
|
150
|
+
row
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async update(tableName, primaryKey, column, value) {
|
|
154
|
+
return this.request("POST", "/mutate", {
|
|
155
|
+
tableName,
|
|
156
|
+
operation: "update",
|
|
157
|
+
primaryKey,
|
|
158
|
+
column,
|
|
159
|
+
value
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async delete(tableName, primaryKey) {
|
|
163
|
+
return this.request("POST", "/mutate", {
|
|
164
|
+
tableName,
|
|
165
|
+
operation: "delete",
|
|
166
|
+
primaryKey
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// ─── Storage Operations ─────────────────────────────────────
|
|
170
|
+
async listFiles(options) {
|
|
171
|
+
const params = {};
|
|
172
|
+
if (options?.prefix) params.prefix = options.prefix;
|
|
173
|
+
if (options?.cursor) params.cursor = options.cursor;
|
|
174
|
+
return this.request("GET", "/storage", void 0, params);
|
|
175
|
+
}
|
|
176
|
+
async getUploadUrl(key, contentType, size) {
|
|
177
|
+
return this.request("POST", "/storage/upload", { key, contentType, size });
|
|
178
|
+
}
|
|
179
|
+
async getDownloadUrl(key) {
|
|
180
|
+
return this.request("GET", "/storage/download", void 0, { key });
|
|
181
|
+
}
|
|
182
|
+
async deleteFiles(keys) {
|
|
183
|
+
return this.request("DELETE", "/storage/objects", { keys });
|
|
184
|
+
}
|
|
185
|
+
async createFolder(path) {
|
|
186
|
+
return this.request("POST", "/storage/folder", { path });
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/server/kysely.ts
|
|
191
|
+
import "kysely";
|
|
192
|
+
async function createDataStore(options) {
|
|
193
|
+
const connectionString = options?.connectionString ?? (typeof process !== "undefined" ? process.env?.DATA_STORE_URL : void 0);
|
|
194
|
+
if (!connectionString) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
"DATA_STORE_URL environment variable is required, or pass connectionString in options"
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const { Kysely: KyselyClass } = await import("kysely");
|
|
200
|
+
const { NeonDialect } = await import("kysely-neon");
|
|
201
|
+
const { neon } = await import("@neondatabase/serverless");
|
|
202
|
+
const sql = neon(connectionString);
|
|
203
|
+
const dialect = new NeonDialect({ neon: sql });
|
|
204
|
+
return new KyselyClass({
|
|
205
|
+
dialect,
|
|
206
|
+
...options?.kyselyConfig
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
export {
|
|
210
|
+
DataStoreClient,
|
|
211
|
+
createDataStore,
|
|
212
|
+
signDeploymentRequest
|
|
213
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
interface DataStoreClientConfig {
|
|
2
|
+
/** Stardeck platform API base URL (default: CONTROL_PLANE_URL env var) */
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
storeId: string;
|
|
5
|
+
/** HMAC signing secret (default: DEPLOYMENT_SECRET env var) */
|
|
6
|
+
deploymentSecret?: string;
|
|
7
|
+
organizationId?: string;
|
|
8
|
+
projectId?: string;
|
|
9
|
+
deploymentId?: string;
|
|
10
|
+
maxRetries?: number;
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface ColumnDefinition {
|
|
14
|
+
name: string;
|
|
15
|
+
fieldType: string;
|
|
16
|
+
nullable?: boolean;
|
|
17
|
+
displayName?: string;
|
|
18
|
+
config?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
interface TableColumn {
|
|
21
|
+
columnName: string;
|
|
22
|
+
dataType: string;
|
|
23
|
+
isNullable: boolean;
|
|
24
|
+
columnDefault: string | null;
|
|
25
|
+
isPrimaryKey: boolean;
|
|
26
|
+
displayName?: string;
|
|
27
|
+
fieldType?: string;
|
|
28
|
+
fieldConfig?: Record<string, unknown> | null;
|
|
29
|
+
position?: number;
|
|
30
|
+
}
|
|
31
|
+
interface TableSchema {
|
|
32
|
+
tableName: string;
|
|
33
|
+
columns: TableColumn[];
|
|
34
|
+
}
|
|
35
|
+
type FilterOperator = "eq" | "neq" | "contains" | "gt" | "lt" | "is_null" | "is_not_null";
|
|
36
|
+
interface QueryFilter {
|
|
37
|
+
column: string;
|
|
38
|
+
operator: FilterOperator;
|
|
39
|
+
value?: unknown;
|
|
40
|
+
}
|
|
41
|
+
interface QueryOptions {
|
|
42
|
+
filters?: QueryFilter[];
|
|
43
|
+
orderBy?: string;
|
|
44
|
+
orderDir?: "asc" | "desc";
|
|
45
|
+
limit?: number;
|
|
46
|
+
offset?: number;
|
|
47
|
+
}
|
|
48
|
+
interface QueryResult {
|
|
49
|
+
rows: Record<string, unknown>[];
|
|
50
|
+
columns: string[];
|
|
51
|
+
total: number;
|
|
52
|
+
limit: number;
|
|
53
|
+
offset: number;
|
|
54
|
+
}
|
|
55
|
+
interface StorageObject {
|
|
56
|
+
key: string;
|
|
57
|
+
size: number;
|
|
58
|
+
lastModified: string;
|
|
59
|
+
contentType?: string;
|
|
60
|
+
}
|
|
61
|
+
interface ListObjectsResult {
|
|
62
|
+
objects: StorageObject[];
|
|
63
|
+
folders: string[];
|
|
64
|
+
cursor?: string;
|
|
65
|
+
hasMore: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type { ColumnDefinition as C, DataStoreClientConfig as D, FilterOperator as F, ListObjectsResult as L, QueryFilter as Q, StorageObject as S, TableColumn as T, TableSchema as a, QueryOptions as b, QueryResult as c };
|