@apexkit/cli 1.0.1 → 1.0.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +128 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apexkit/cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "The official CLI and developer sync engine for ApexKit",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -141,7 +141,7 @@ const TRIGGER_DEFINITIONS = [
141
141
  { id: "graphql", name: "graphql (Custom Resolver)", desc: "Dynamic Query/Mutation field resolver for GraphQL schema." }
142
142
  ];
143
143
 
144
- // --- 4. WORKSPACE TYPES & CONFIG GENERATOR ---
144
+ // --- 4. ACCURATE WORKSPACE TYPES & CONFIG GENERATOR ---
145
145
  async function generateWorkspaceFiles() {
146
146
  console.log(" ⏳ Fetching collection schemas from DB...");
147
147
  let collectionsData = [];
@@ -155,6 +155,34 @@ async function generateWorkspaceFiles() {
155
155
  console.warn(` ⚠️ Could not connect to DB for schemas: ${err.message}`);
156
156
  }
157
157
 
158
+ // Pre-calculate lookup maps for collection IDs and Names
159
+ const colIdToName = new Map();
160
+ const colNameToId = new Map();
161
+ collectionsData.forEach((col) => {
162
+ colIdToName.set(String(col.id), col.name);
163
+ colNameToId.set(col.name, col.id);
164
+ });
165
+
166
+ // Calculate Reverse Relations: Target Collection Name -> Array of Reverse Link Definitions
167
+ const reverseRelationsMap = new Map();
168
+ collectionsData.forEach((originCol) => {
169
+ if (originCol.schema?.relations) {
170
+ for (const [relName, relDef] of Object.entries(originCol.schema.relations)) {
171
+ const rawTarget = relDef.target_collection;
172
+ const targetName = colIdToName.get(String(rawTarget)) || rawTarget;
173
+
174
+ if (!reverseRelationsMap.has(targetName)) {
175
+ reverseRelationsMap.set(targetName, []);
176
+ }
177
+ reverseRelationsMap.get(targetName).push({
178
+ originCollection: originCol.name,
179
+ relationFieldName: relName,
180
+ cardinality: relDef.relation_type, // 'many' or 'one'
181
+ });
182
+ }
183
+ }
184
+ });
185
+
158
186
  let collectionTypesStr = `export interface Collections {\n`;
159
187
  let collectionExpandsStr = `export interface CollectionExpands {\n`;
160
188
  const collectionNames = [];
@@ -165,6 +193,7 @@ async function generateWorkspaceFiles() {
165
193
  let expandsStr = "";
166
194
 
167
195
  if (col.schema) {
196
+ // 1. STANDARD FIELDS
168
197
  if (col.schema.fields) {
169
198
  for (const [name, def] of Object.entries(col.schema.fields)) {
170
199
  let tsType = "any";
@@ -183,6 +212,7 @@ async function generateWorkspaceFiles() {
183
212
  break;
184
213
  case "owner":
185
214
  case "relation":
215
+ // ✅ Foreign keys and owners are ALWAYS scalar IDs on the record itself
186
216
  tsType = "number | string";
187
217
  break;
188
218
  case "boolean":
@@ -206,21 +236,59 @@ async function generateWorkspaceFiles() {
206
236
  }
207
237
  const safeName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
208
238
  fieldsStr += ` ${safeName}${def.required ? "" : "?"}: ${tsType};\n`;
239
+
240
+ // If it's an Owner field, it expands to the User Profile map
241
+ if (def.type === "owner") {
242
+ expandsStr += ` ${safeName}?: { id: number | string; email: string; role: string; metadata?: Record<string, any> };\n`;
243
+ }
209
244
  }
210
245
  }
211
246
 
247
+ // 2. FORWARD RELATIONS (Directly on this collection)
212
248
  if (col.schema.relations) {
213
249
  for (const [name, rel] of Object.entries(col.schema.relations)) {
214
- const isMany = rel.relation_type === "many";
215
- const tsType = isMany ? "(number | string)[]" : "number | string";
250
+ const rawTarget = rel.target_collection;
251
+ const targetColName = colIdToName.get(String(rawTarget)) || rawTarget;
216
252
  const safeName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
217
- fieldsStr += ` ${safeName}${rel.required ? "" : "?"}: ${tsType};\n`;
218
253
 
219
- const targetCol = rel.target_collection;
220
- const expandType = `{ id: number | string; data: Collections["${targetCol}"]; created: string; updated: string; expand?: any }`;
221
- expandsStr += ` ${safeName}?: ${isMany ? `Array<${expandType}>` : expandType};\n`;
254
+ // ✅ BULLETPROOF RULE 1:
255
+ // A record holding the foreign key field stores a SCALAR foreign key ID, NEVER an array.
256
+ fieldsStr += ` ${safeName}${rel.required ? "" : "?"}: number | string;\n`;
257
+
258
+ // ✅ BULLETPROOF RULE 2:
259
+ // Forward expansion from the foreign key field points to ONE target record.
260
+ // It is ALWAYS a Map/Object, NEVER an array!
261
+ const targetRecordType = `Collections["${targetColName}"]`;
262
+ const expandSingleType = `{ id: number | string; data: ${targetRecordType}; created: string; updated: string; expand?: any }`;
263
+
264
+ expandsStr += ` ${safeName}?: ${expandSingleType};\n`;
222
265
  }
223
266
  }
267
+
268
+ // 3. REVERSE RELATIONS (Parent expanding children)
269
+ const reverseLinks = reverseRelationsMap.get(col.name) || [];
270
+ const fieldCountByName = new Map();
271
+ reverseLinks.forEach((rev) => {
272
+ fieldCountByName.set(rev.originCollection, (fieldCountByName.get(rev.originCollection) || 0) + 1);
273
+ });
274
+
275
+ reverseLinks.forEach((rev) => {
276
+ const isDuplicate = (fieldCountByName.get(rev.originCollection) || 0) > 1;
277
+ const revKey = isDuplicate ? `${rev.originCollection}_via_${rev.relationFieldName}` : rev.originCollection;
278
+ const safeRevKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(revKey) ? revKey : `"${revKey}"`;
279
+
280
+ const originRecordType = `Collections["${rev.originCollection}"]`;
281
+ const childRecordType = `{ id: number | string; data: ${originRecordType}; created: string; updated: string; expand?: any }`;
282
+
283
+ // ✅ BULLETPROOF RULE 3:
284
+ // Only return an Array when reverse-expanded from parent AND cardinality is 'many'.
285
+ // If the reverse relation is unique ('one'), return a single Map/Object.
286
+ if (rev.cardinality === "one") {
287
+ expandsStr += ` ${safeRevKey}?: ${childRecordType};\n`;
288
+ } else {
289
+ expandsStr += ` ${safeRevKey}?: Array<${childRecordType}>;\n`;
290
+ }
291
+ });
224
292
  }
225
293
 
226
294
  collectionTypesStr += ` "${col.name}": {\n${fieldsStr} };\n`;
@@ -460,10 +528,58 @@ declare global {
460
528
  base64EncodeBuffer(buffer: ArrayBuffer | Uint8Array): string;
461
529
  base64Decode(text: string): string;
462
530
  base64DecodeBuffer(text: string): ArrayBuffer;
531
+ textEncode(text: string): ArrayBuffer;
532
+ textDecode(data: ArrayBuffer | Uint8Array): string;
533
+ hexEncode(data: string | ArrayBuffer | Uint8Array): string;
534
+ hexDecode(hex: string): ArrayBuffer;
463
535
  sleep(ms: number): Promise<void>;
464
536
  randomHex(len?: number): string;
465
537
  };
466
538
 
539
+ /** Standard Web APIs */
540
+ const crypto: {
541
+ randomUUID(): string;
542
+ getRandomValues<T extends Int8Array | Int16Array | Int32Array | Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array | DataView | null>(array: T): T;
543
+ subtle: {
544
+ digest(algorithm: string | any, data: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>;
545
+ importKey(format: string, keyData: any, algo: any, extractable: boolean, usages: string[]): Promise<any>;
546
+ sign(algorithm: string | any, key: any, data: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>;
547
+ verify(algorithm: string | any, key: any, signature: ArrayBuffer | Uint8Array, data: ArrayBuffer | Uint8Array): Promise<boolean>;
548
+ }
549
+ };
550
+
551
+ class Blob {
552
+ constructor(sources?: any[], options?: any);
553
+ arrayBuffer(): Promise<ArrayBuffer>;
554
+ text(): Promise<string>;
555
+ readonly size: number;
556
+ readonly type: string;
557
+ }
558
+
559
+ class ReadableStream {
560
+ constructor(underlyingSource?: any);
561
+ getReader(): any;
562
+ cancel(): Promise<void>;
563
+ readonly locked: boolean;
564
+ }
565
+
566
+ const btoa: (data: string) => string;
567
+ const atob: (data: string) => string;
568
+
569
+ class TextEncoder {
570
+ constructor(encoding?: string);
571
+ encode(input?: string): Uint8Array;
572
+ encodeInto(source: string, destination: Uint8Array): { read: number; written: number };
573
+ }
574
+
575
+ class TextDecoder {
576
+ constructor(label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean });
577
+ decode(input?: ArrayBuffer | Uint8Array, options?: { stream?: boolean }): string;
578
+ }
579
+
580
+ function setTimeout(callback: Function, ms: number): string;
581
+ function clearTimeout(id: string): void;
582
+
467
583
  /** Environment & Configuration Secrets Accessor */
468
584
  const $env: {
469
585
  get(key: string): Promise<string>;
@@ -484,7 +600,7 @@ declare global {
484
600
  export {};
485
601
  `;
486
602
  fs.writeFileSync("apexkit.d.ts", types.trim());
487
- console.log(" 📄 Generated apexkit.d.ts with Engine & Collection Typings");
603
+ console.log(" 📄 Generated apexkit.d.ts with Accurate Scaled & Relational Typings");
488
604
 
489
605
  const jsconfig = {
490
606
  compilerOptions: {
@@ -566,7 +682,7 @@ export default async function (context) {
566
682
  <head>
567
683
  <meta charset="UTF-8">
568
684
  <title>{{ title }}</title>
569
- <link rel="stylesheet" href="/styles.css">
685
+ <script src="https://cdn.tailwindcss.com"></script>
570
686
  <script src="/static/js/htmx.js"></script>
571
687
  <script src="/static/js/alpine.js" defer></script>
572
688
  </head>
@@ -575,10 +691,10 @@ export default async function (context) {
575
691
  <h1 class="text-3xl font-bold text-indigo-400">{{ title }}</h1>
576
692
 
577
693
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
578
- {% for item in items %}
694
+ {% for post in posts %}
579
695
  <div class="p-4 bg-slate-800 rounded-lg border border-slate-700 shadow">
580
- <h2 class="text-xl font-semibold">{{ item.data.title | default(value="Item #" ~ item.id) }}</h2>
581
- <p class="text-slate-400 text-sm mt-2">{{ item.created }}</p>
696
+ <h2 class="text-xl font-semibold">{{ post.data.title | default(value="Item #" ~ post.id) }}</h2>
697
+ <p class="text-slate-400 text-sm mt-2">{{ post.created }}</p>
582
698
  </div>
583
699
  {% endfor %}
584
700
  </div>