@apexkit/cli 1.0.0 → 1.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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/index.js +80 -12
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apexkit/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "The official CLI and developer sync engine for ApexKit",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "bin": {
8
- "apexkit": "./bin/apexkit.js",
9
- "apex": "./bin/apexkit.js"
8
+ "apexkit": "bin/apexkit.js",
9
+ "apex": "bin/apexkit.js"
10
10
  },
11
11
  "scripts": {
12
12
  "test": "echo \"Error: no test specified\" && exit 1"
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`;
@@ -484,7 +552,7 @@ declare global {
484
552
  export {};
485
553
  `;
486
554
  fs.writeFileSync("apexkit.d.ts", types.trim());
487
- console.log(" 📄 Generated apexkit.d.ts with Engine & Collection Typings");
555
+ console.log(" 📄 Generated apexkit.d.ts with Accurate Scaled & Relational Typings");
488
556
 
489
557
  const jsconfig = {
490
558
  compilerOptions: {
@@ -566,7 +634,7 @@ export default async function (context) {
566
634
  <head>
567
635
  <meta charset="UTF-8">
568
636
  <title>{{ title }}</title>
569
- <link rel="stylesheet" href="/styles.css">
637
+ <script src="https://cdn.tailwindcss.com"></script>
570
638
  <script src="/static/js/htmx.js"></script>
571
639
  <script src="/static/js/alpine.js" defer></script>
572
640
  </head>
@@ -575,10 +643,10 @@ export default async function (context) {
575
643
  <h1 class="text-3xl font-bold text-indigo-400">{{ title }}</h1>
576
644
 
577
645
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
578
- {% for item in items %}
646
+ {% for post in posts %}
579
647
  <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>
648
+ <h2 class="text-xl font-semibold">{{ post.data.title | default(value="Item #" ~ post.id) }}</h2>
649
+ <p class="text-slate-400 text-sm mt-2">{{ post.created }}</p>
582
650
  </div>
583
651
  {% endfor %}
584
652
  </div>