@stardeck-customer-apps/data-store-sdk 0.1.0-preview.0 → 0.1.0-preview.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/SKILL.md ADDED
@@ -0,0 +1,294 @@
1
+ # Data Store SDK - AI Agent Skill Guide
2
+
3
+ This guide helps AI agents understand and implement data store features using `@stardeck-customer-apps/data-store-sdk`.
4
+
5
+ ## Quick Reference
6
+
7
+ **Package**: `@stardeck-customer-apps/data-store-sdk`
8
+
9
+ **Exports**:
10
+
11
+ - Core: `@stardeck-customer-apps/data-store-sdk` - Types and error classes
12
+ - Server: `@stardeck-customer-apps/data-store-sdk/server` - DataStoreClient, Kysely wrapper, HMAC signing
13
+
14
+ **Two Access Patterns**:
15
+
16
+ 1. **DataStoreClient** — Platform API client for schema management and dynamic queries (no generated types needed)
17
+ 2. **createDataStore()** — Direct Kysely connection for type-safe queries (requires generated types + DATA_STORE_URL)
18
+
19
+ ## Architecture Overview
20
+
21
+ **Schema Management (via Platform API):**
22
+
23
+ ```
24
+ Server Code
25
+ ↓ (DataStoreClient with HMAC auth)
26
+ Platform API (/api/data-stores/{storeId}/schema/*)
27
+
28
+ Data Store Neon Database (DDL applied)
29
+ ```
30
+
31
+ **Dynamic Queries (via Platform API):**
32
+
33
+ ```
34
+ Server Code
35
+ ↓ (DataStoreClient with HMAC auth)
36
+ Platform API (/api/data-stores/{storeId}/query or /mutate)
37
+
38
+ Data Store Neon Database
39
+ ```
40
+
41
+ **Type-Safe Queries (Direct Connection):**
42
+
43
+ ```
44
+ Server Code
45
+ ↓ (Kysely with DATA_STORE_URL)
46
+ Data Store Neon Database (direct connection)
47
+ ```
48
+
49
+ ## Setup
50
+
51
+ ### 1. Install Dependencies
52
+
53
+ ```bash
54
+ npm install @stardeck-customer-apps/data-store-sdk
55
+ # For direct Kysely access (optional):
56
+ npm install kysely kysely-neon @neondatabase/serverless
57
+ ```
58
+
59
+ ### 2. Environment Variables
60
+
61
+ These are automatically injected by the platform at deploy time:
62
+
63
+ ```bash
64
+ CONTROL_PLANE_URL=https://app.stardeck.ai # Platform API URL
65
+ DEPLOYMENT_SECRET=your-secret-key # HMAC signing (NEVER expose to client)
66
+ ORGANIZATION_ID=org_123
67
+ PROJECT_ID=proj_456
68
+ DEPLOYMENT_ID=deploy_789
69
+ DATA_STORE_MYSTORE_URL=postgres://... # Per-store connection string (for Kysely)
70
+ DATA_STORE_MYSTORE_ID=ds-abc123 # Per-store ID (for DataStoreClient)
71
+ ```
72
+
73
+ ## Pattern 1: DataStoreClient (Dynamic Queries)
74
+
75
+ Use this for schema operations, agent-driven data access, or when you don't have generated types.
76
+
77
+ ### Initialize
78
+
79
+ ```typescript
80
+ // lib/data-store.ts (server-only)
81
+ import { DataStoreClient } from "@stardeck-customer-apps/data-store-sdk/server";
82
+
83
+ export const dataStore = new DataStoreClient({
84
+ storeId: process.env.DATA_STORE_MYSTORE_ID!,
85
+ // Other config read from env vars automatically
86
+ });
87
+ ```
88
+
89
+ ### Schema Operations
90
+
91
+ ```typescript
92
+ // Create a table
93
+ await dataStore.createTable("orders", [
94
+ { name: "customer_name", fieldType: "text" },
95
+ { name: "total", fieldType: "currency", config: { currency: "USD" } },
96
+ { name: "status", fieldType: "select", config: { options: ["pending", "shipped", "delivered"] } },
97
+ { name: "notes", fieldType: "long_text", nullable: true },
98
+ ]);
99
+
100
+ // Add a column to an existing table
101
+ await dataStore.addColumn("orders", {
102
+ name: "priority",
103
+ fieldType: "rating",
104
+ config: { max: 5 },
105
+ });
106
+
107
+ // Read schema
108
+ const { tables } = await dataStore.getSchema();
109
+ ```
110
+
111
+ ### Available Field Types
112
+
113
+ | Field Type | Postgres Type | Config Options |
114
+ | -------------- | ------------- | ------------------------------------------------- |
115
+ | `text` | text | — |
116
+ | `long_text` | text | — |
117
+ | `number` | numeric | — |
118
+ | `boolean` | boolean | — |
119
+ | `date` | date | — |
120
+ | `datetime` | timestamptz | — |
121
+ | `select` | text | `{ options: string[] }` |
122
+ | `multi_select` | text[] | `{ options: string[] }` |
123
+ | `currency` | numeric | `{ currency: string }` |
124
+ | `email` | text | — |
125
+ | `phone` | text | — |
126
+ | `url` | text | — |
127
+ | `rating` | integer | `{ max: number }` |
128
+ | `relation` | uuid | `{ targetTable: string, displayColumn?: string }` |
129
+ | `file_ref` | text | — |
130
+ | `json` | jsonb | — |
131
+
132
+ ### Query Data
133
+
134
+ ```typescript
135
+ // Simple query
136
+ const { rows, total } = await dataStore.query("orders", {
137
+ filters: [
138
+ { column: "status", operator: "eq", value: "pending" },
139
+ ],
140
+ orderBy: "created_at",
141
+ orderDir: "desc",
142
+ limit: 20,
143
+ });
144
+
145
+ // Available filter operators: eq, neq, gt, lt, contains, is_null, is_not_null
146
+ ```
147
+
148
+ ### Mutate Data
149
+
150
+ ```typescript
151
+ // Insert
152
+ const { row } = await dataStore.insert("orders", {
153
+ customer_name: "Alice",
154
+ total: 99.99,
155
+ status: "pending",
156
+ });
157
+
158
+ // Update (by primary key)
159
+ await dataStore.update("orders", { id: row.id }, "status", "shipped");
160
+
161
+ // Delete (by primary key)
162
+ await dataStore.delete("orders", { id: row.id });
163
+ ```
164
+
165
+ ### Storage Operations (for storage-type data stores)
166
+
167
+ > **Not the Storage SDK.** Storage-type data stores are org-level shared file storage accessed via `DataStoreClient` (this SDK). They are a completely separate system from the legacy Storage SDK (`@stardeck-customer-apps/storage-sdk`). Do not confuse the two:
168
+ >
169
+ > - **Storage SDK (legacy)** = project-scoped file storage for a single app. Being phased out in favor of storage-type data stores.
170
+ > - **Data Store storage** = org-scoped shared file storage accessed by multiple projects via `DataStoreClient`. This is the recommended approach for file storage.
171
+ >
172
+ > Always prefer data store storage over the Storage SDK for new code.
173
+
174
+ ```typescript
175
+ // List files
176
+ const { objects, folders } = await dataStore.listFiles({ prefix: "invoices/" });
177
+
178
+ // Get upload URL (client uploads directly to R2)
179
+ const { uploadUrl } = await dataStore.getUploadUrl("invoices/march.pdf", "application/pdf", fileSize);
180
+
181
+ // Get download URL
182
+ const { downloadUrl } = await dataStore.getDownloadUrl("invoices/march.pdf");
183
+
184
+ // Delete files
185
+ await dataStore.deleteFiles(["invoices/old.pdf"]);
186
+ ```
187
+
188
+ ## Pattern 2: Kysely (Type-Safe Queries)
189
+
190
+ Use this for application code where you want compile-time type safety.
191
+
192
+ ### Generate Types
193
+
194
+ ```bash
195
+ npx stardeck-data-store generate-types --connection-string $DATA_STORE_MYSTORE_URL
196
+ # Outputs: ./src/generated/data-store-types.ts
197
+ ```
198
+
199
+ ### Initialize
200
+
201
+ ```typescript
202
+ // lib/db.ts (server-only)
203
+ import { createDataStore } from "@stardeck-customer-apps/data-store-sdk/server";
204
+ import type { DB } from "@/generated/data-store-types";
205
+
206
+ export const db = await createDataStore<DB>({
207
+ connectionString: process.env.DATA_STORE_MYSTORE_URL,
208
+ });
209
+ ```
210
+
211
+ ### Query with Full Type Safety
212
+
213
+ ```typescript
214
+ import { db } from "@/lib/db";
215
+
216
+ // Select with type inference
217
+ const orders = await db
218
+ .selectFrom("orders")
219
+ .where("status", "=", "pending")
220
+ .orderBy("created_at", "desc")
221
+ .selectAll()
222
+ .execute();
223
+
224
+ // Insert
225
+ const newOrder = await db
226
+ .insertInto("orders")
227
+ .values({
228
+ customer_name: "Bob",
229
+ total: 149.99,
230
+ status: "pending",
231
+ })
232
+ .returningAll()
233
+ .executeTakeFirstOrThrow();
234
+
235
+ // Update
236
+ await db
237
+ .updateTable("orders")
238
+ .set({ status: "shipped" })
239
+ .where("id", "=", orderId)
240
+ .execute();
241
+
242
+ // Join
243
+ const ordersWithCustomers = await db
244
+ .selectFrom("orders")
245
+ .innerJoin("customers", "customers.id", "orders.customer_id")
246
+ .select(["orders.id", "orders.total", "customers.name"])
247
+ .execute();
248
+ ```
249
+
250
+ ## Error Handling
251
+
252
+ ```typescript
253
+ import {
254
+ DataStoreError,
255
+ AuthenticationError,
256
+ ForbiddenError,
257
+ ValidationError,
258
+ NotFoundError,
259
+ } from "@stardeck-customer-apps/data-store-sdk";
260
+
261
+ try {
262
+ await dataStore.query("orders");
263
+ } catch (error) {
264
+ if (error instanceof AuthenticationError) {
265
+ // HMAC signature invalid or expired
266
+ } else if (error instanceof ForbiddenError) {
267
+ // Insufficient access level for this operation
268
+ } else if (error instanceof ValidationError) {
269
+ // Invalid request (bad table name, invalid filter, etc.)
270
+ } else if (error instanceof NotFoundError) {
271
+ // Data store or table not found
272
+ } else if (error instanceof DataStoreError) {
273
+ // General API error — check error.code and error.statusCode
274
+ }
275
+ }
276
+ ```
277
+
278
+ ## When to Use Which Pattern
279
+
280
+ | Scenario | Pattern |
281
+ | ----------------------------------------- | ------------------ |
282
+ | Application CRUD with known schema | Kysely (type-safe) |
283
+ | Agent building features dynamically | DataStoreClient |
284
+ | Schema management (create/alter tables) | DataStoreClient |
285
+ | Edge functions / serverless | DataStoreClient |
286
+ | Complex queries (joins, CTEs, subqueries) | Kysely |
287
+ | Storage file operations | DataStoreClient |
288
+
289
+ ## Important Notes
290
+
291
+ - **DEPLOYMENT_SECRET must never be exposed to the client.** All data store operations must happen server-side.
292
+ - **Tables auto-generate** `id` (UUID), `created_at`, and `updated_at` columns. Don't include these in create table definitions.
293
+ - **Column deletes are soft-deletes.** The column is renamed to `_deleted_{name}_{timestamp}` and can be restored.
294
+ - **Type changes are restricted** to safe widening conversions (e.g., integer → bigint, boolean → text).
@@ -53,7 +53,7 @@ var PG_TYPE_MAP = {
53
53
  "time with time zone": "string",
54
54
  "time without time zone": "string",
55
55
  bytea: "Buffer",
56
- "ARRAY": "unknown[]"
56
+ ARRAY: "unknown[]"
57
57
  };
58
58
  function pgTypeToTs(pgType) {
59
59
  return PG_TYPE_MAP[pgType] ?? "unknown";
@@ -30,7 +30,7 @@ var PG_TYPE_MAP = {
30
30
  "time with time zone": "string",
31
31
  "time without time zone": "string",
32
32
  bytea: "Buffer",
33
- "ARRAY": "unknown[]"
33
+ ARRAY: "unknown[]"
34
34
  };
35
35
  function pgTypeToTs(pgType) {
36
36
  return PG_TYPE_MAP[pgType] ?? "unknown";
@@ -90,9 +90,16 @@ var DataStoreClient = class {
90
90
  this.deploymentId = config.deploymentId || this.getEnv("DEPLOYMENT_ID") || "";
91
91
  this.maxRetries = config.maxRetries ?? 3;
92
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."
93
+ const missing = [
94
+ !this.baseUrl && "CONTROL_PLANE_URL",
95
+ !this.deploymentSecret && "DEPLOYMENT_SECRET",
96
+ !this.organizationId && "ORGANIZATION_ID",
97
+ !this.projectId && "PROJECT_ID",
98
+ !this.deploymentId && "DEPLOYMENT_ID"
99
+ ].filter(Boolean);
100
+ if (missing.length > 0) {
101
+ throw new Error(
102
+ `[DataStoreClient] Missing required configuration: ${missing.join(", ")}. Pass them in the config object or set the corresponding environment variables.`
96
103
  );
97
104
  }
98
105
  }
@@ -156,6 +163,8 @@ var DataStoreClient = class {
156
163
  if (error instanceof AuthenticationError) throw error;
157
164
  if (error instanceof DataStoreError && error.statusCode < 500) throw error;
158
165
  if (attempt < this.maxRetries) {
166
+ const isIdempotent = method === "GET" || method === "HEAD";
167
+ if (!isIdempotent) throw lastError ?? error;
159
168
  const delay = Math.min(1e3 * 2 ** attempt, 1e4);
160
169
  await new Promise((r) => setTimeout(r, delay));
161
170
  }
@@ -163,7 +172,6 @@ var DataStoreClient = class {
163
172
  }
164
173
  throw lastError ?? new DataStoreError("Request failed", "UNKNOWN", 500);
165
174
  }
166
- // ─── Schema Operations ──────────────────────────────────────
167
175
  async getSchema() {
168
176
  return this.request("GET", "/schema");
169
177
  }
@@ -183,7 +191,6 @@ var DataStoreClient = class {
183
191
  async deleteColumn(tableName, columnName) {
184
192
  return this.request("DELETE", "/schema/columns", { tableName, columnName });
185
193
  }
186
- // ─── Query Operations ───────────────────────────────────────
187
194
  async query(tableName, options = {}) {
188
195
  return this.request("POST", "/query", {
189
196
  tableName,
@@ -217,7 +224,6 @@ var DataStoreClient = class {
217
224
  primaryKey
218
225
  });
219
226
  }
220
- // ─── Storage Operations ─────────────────────────────────────
221
227
  async listFiles(options) {
222
228
  const params = {};
223
229
  if (options?.prefix) params.prefix = options.prefix;
@@ -39,9 +39,16 @@ var DataStoreClient = class {
39
39
  this.deploymentId = config.deploymentId || this.getEnv("DEPLOYMENT_ID") || "";
40
40
  this.maxRetries = config.maxRetries ?? 3;
41
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."
42
+ const missing = [
43
+ !this.baseUrl && "CONTROL_PLANE_URL",
44
+ !this.deploymentSecret && "DEPLOYMENT_SECRET",
45
+ !this.organizationId && "ORGANIZATION_ID",
46
+ !this.projectId && "PROJECT_ID",
47
+ !this.deploymentId && "DEPLOYMENT_ID"
48
+ ].filter(Boolean);
49
+ if (missing.length > 0) {
50
+ throw new Error(
51
+ `[DataStoreClient] Missing required configuration: ${missing.join(", ")}. Pass them in the config object or set the corresponding environment variables.`
45
52
  );
46
53
  }
47
54
  }
@@ -105,6 +112,8 @@ var DataStoreClient = class {
105
112
  if (error instanceof AuthenticationError) throw error;
106
113
  if (error instanceof DataStoreError && error.statusCode < 500) throw error;
107
114
  if (attempt < this.maxRetries) {
115
+ const isIdempotent = method === "GET" || method === "HEAD";
116
+ if (!isIdempotent) throw lastError ?? error;
108
117
  const delay = Math.min(1e3 * 2 ** attempt, 1e4);
109
118
  await new Promise((r) => setTimeout(r, delay));
110
119
  }
@@ -112,7 +121,6 @@ var DataStoreClient = class {
112
121
  }
113
122
  throw lastError ?? new DataStoreError("Request failed", "UNKNOWN", 500);
114
123
  }
115
- // ─── Schema Operations ──────────────────────────────────────
116
124
  async getSchema() {
117
125
  return this.request("GET", "/schema");
118
126
  }
@@ -132,7 +140,6 @@ var DataStoreClient = class {
132
140
  async deleteColumn(tableName, columnName) {
133
141
  return this.request("DELETE", "/schema/columns", { tableName, columnName });
134
142
  }
135
- // ─── Query Operations ───────────────────────────────────────
136
143
  async query(tableName, options = {}) {
137
144
  return this.request("POST", "/query", {
138
145
  tableName,
@@ -166,7 +173,6 @@ var DataStoreClient = class {
166
173
  primaryKey
167
174
  });
168
175
  }
169
- // ─── Storage Operations ─────────────────────────────────────
170
176
  async listFiles(options) {
171
177
  const params = {};
172
178
  if (options?.prefix) params.prefix = options.prefix;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/data-store-sdk",
3
- "version": "0.1.0-preview.0",
3
+ "version": "0.1.0-preview.2",
4
4
  "description": "SDK for accessing Stardeck data stores from deployed projects",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -13,7 +13,8 @@
13
13
  "stardeck-data-store": "./dist/cli/generate-types.js"
14
14
  },
15
15
  "files": [
16
- "dist"
16
+ "dist",
17
+ "SKILL.md"
17
18
  ],
18
19
  "exports": {
19
20
  ".": {