@stardeck-customer-apps/data-store-sdk 0.1.0-preview.1 → 0.1.0-preview.3

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