algolia-codegen 0.1.0 → 0.1.1

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.
@@ -0,0 +1,558 @@
1
+ // src/utils/config-loader.ts
2
+ import { existsSync } from "fs";
3
+ import { resolve } from "path";
4
+ import { pathToFileURL } from "url";
5
+
6
+ // src/utils/validations/generator-config.ts
7
+ function validateGeneratorConfig(config, path) {
8
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
9
+ throw new Error(
10
+ `Invalid generator config: must be an object
11
+ Path: ${path}`
12
+ );
13
+ }
14
+ const cfg = config;
15
+ const requiredFields = [
16
+ "appId",
17
+ "searchKey",
18
+ "indexName"
19
+ ];
20
+ for (const field of requiredFields) {
21
+ if (!(field in cfg)) {
22
+ throw new Error(
23
+ `Invalid generator config: missing required property '${field}'
24
+ Path: ${path}`
25
+ );
26
+ }
27
+ if (typeof cfg[field] !== "string") {
28
+ throw new Error(
29
+ `Invalid generator config: '${field}' must be a string
30
+ Path: ${path}
31
+ Received: ${typeof cfg[field]}`
32
+ );
33
+ }
34
+ }
35
+ if ("prefix" in cfg && cfg.prefix !== void 0 && typeof cfg.prefix !== "string") {
36
+ throw new Error(
37
+ `Invalid generator config: 'prefix' must be a string or undefined
38
+ Path: ${path}
39
+ Received: ${typeof cfg.prefix}`
40
+ );
41
+ }
42
+ if ("postfix" in cfg && cfg.postfix !== void 0 && typeof cfg.postfix !== "string") {
43
+ throw new Error(
44
+ `Invalid generator config: 'postfix' must be a string or undefined
45
+ Path: ${path}
46
+ Received: ${typeof cfg.postfix}`
47
+ );
48
+ }
49
+ }
50
+
51
+ // src/utils/validations/url-schema.ts
52
+ function validateUrlSchema(urlSchema, path) {
53
+ if (typeof urlSchema !== "object" || urlSchema === null || Array.isArray(urlSchema)) {
54
+ throw new Error(
55
+ `Invalid generates entry: must be an object
56
+ Path: ${path}`
57
+ );
58
+ }
59
+ const schema = urlSchema;
60
+ for (const [filePath, generatorConfig] of Object.entries(schema)) {
61
+ if (typeof filePath !== "string") {
62
+ throw new Error(
63
+ `Invalid generates entry: file path must be a string
64
+ Path: ${path}[${filePath}]`
65
+ );
66
+ }
67
+ validateGeneratorConfig(generatorConfig, `${path}["${filePath}"]`);
68
+ }
69
+ }
70
+
71
+ // src/utils/validations/config.ts
72
+ function validateConfig(config, configPath) {
73
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
74
+ throw new Error(
75
+ `Invalid config: must be an object
76
+ Config file: ${configPath}`
77
+ );
78
+ }
79
+ const cfg = config;
80
+ if (!("overwrite" in cfg)) {
81
+ throw new Error(
82
+ `Invalid config: missing required property 'overwrite'
83
+ Config file: ${configPath}`
84
+ );
85
+ }
86
+ if (typeof cfg.overwrite !== "boolean") {
87
+ throw new Error(
88
+ `Invalid config: 'overwrite' must be a boolean
89
+ Config file: ${configPath}
90
+ Received: ${typeof cfg.overwrite}`
91
+ );
92
+ }
93
+ if (!("generates" in cfg)) {
94
+ throw new Error(
95
+ `Invalid config: missing required property 'generates'
96
+ Config file: ${configPath}`
97
+ );
98
+ }
99
+ const generates = cfg.generates;
100
+ if (Array.isArray(generates)) {
101
+ generates.forEach((item, index) => {
102
+ validateUrlSchema(item, `${configPath}[generates][${index}]`);
103
+ });
104
+ } else if (typeof generates === "object" && generates !== null) {
105
+ validateUrlSchema(generates, `${configPath}[generates]`);
106
+ } else {
107
+ throw new Error(
108
+ `Invalid config: 'generates' must be an object or an array of objects
109
+ Config file: ${configPath}
110
+ Received: ${typeof generates}`
111
+ );
112
+ }
113
+ }
114
+
115
+ // src/utils/config-loader.ts
116
+ async function loadConfig(configPath) {
117
+ const defaultConfigPath = "algolia-codegen.ts";
118
+ const finalConfigPath = configPath || defaultConfigPath;
119
+ const resolvedPath = resolve(process.cwd(), finalConfigPath);
120
+ if (!existsSync(resolvedPath)) {
121
+ throw new Error(
122
+ `Config file not found: ${resolvedPath}
123
+ Please create a config file or specify a different path using --config option.`
124
+ );
125
+ }
126
+ const configUrl = pathToFileURL(resolvedPath).href;
127
+ let configModule;
128
+ try {
129
+ configModule = await import(configUrl);
130
+ } catch (importError) {
131
+ const jsPath = resolvedPath.replace(/\.ts$/, ".js");
132
+ if (existsSync(jsPath)) {
133
+ const jsUrl = pathToFileURL(jsPath).href;
134
+ try {
135
+ configModule = await import(jsUrl);
136
+ } catch (jsImportError) {
137
+ throw new Error(
138
+ `Failed to import config file: ${resolvedPath}
139
+ Tried both .ts and .js extensions.
140
+ Error: ${jsImportError instanceof Error ? jsImportError.message : String(jsImportError)}
141
+ Note: If using TypeScript config, you may need to compile it first or use a tool like tsx.`
142
+ );
143
+ }
144
+ } else {
145
+ const errorMessage = importError instanceof Error ? importError.message : String(importError);
146
+ const isTypeScriptError = resolvedPath.endsWith(".ts") && (errorMessage.includes("Cannot find module") || errorMessage.includes("Unknown file extension"));
147
+ throw new Error(
148
+ `Failed to import config file: ${resolvedPath}
149
+ ` + (isTypeScriptError ? `Node.js cannot directly import TypeScript files.
150
+ Please either:
151
+ 1. Compile your config to JavaScript (.js)
152
+ 2. Use a tool like tsx to run the CLI: tsx algolia-codegen
153
+ 3. Or use a JavaScript config file instead
154
+ ` : `Error: ${errorMessage}`)
155
+ );
156
+ }
157
+ }
158
+ if (!configModule.default) {
159
+ throw new Error(
160
+ `Config file does not export a default object: ${resolvedPath}
161
+ Please ensure your config file exports a default object: export default { ... }`
162
+ );
163
+ }
164
+ const config = configModule.default;
165
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
166
+ throw new Error(
167
+ `Config file default export must be an object: ${resolvedPath}
168
+ Received: ${typeof config}`
169
+ );
170
+ }
171
+ validateConfig(config, resolvedPath);
172
+ return config;
173
+ }
174
+
175
+ // src/utils/fetch-algolia-data.ts
176
+ import algoliasearch from "algoliasearch";
177
+ import { existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
178
+ import { resolve as resolve2, dirname } from "path";
179
+
180
+ // src/utils/generate-typescript-types.ts
181
+ var TypeGenerator = class {
182
+ typeMap = /* @__PURE__ */ new Map();
183
+ generatedTypes = /* @__PURE__ */ new Set();
184
+ idValueTypes = /* @__PURE__ */ new Set();
185
+ prefix;
186
+ postfix;
187
+ indexName;
188
+ constructor(config) {
189
+ this.prefix = config.prefix || "";
190
+ this.postfix = config.postfix || "";
191
+ this.indexName = config.indexName;
192
+ }
193
+ /**
194
+ * Convert a value to its TypeScript type representation
195
+ */
196
+ inferType(value, path = []) {
197
+ if (value === null || value === void 0) {
198
+ return { type: "unknown", isOptional: true, isArray: false, nestedTypes: /* @__PURE__ */ new Map() };
199
+ }
200
+ if (Array.isArray(value)) {
201
+ if (value.length === 0) {
202
+ return { type: "unknown[]", isOptional: false, isArray: true, nestedTypes: /* @__PURE__ */ new Map() };
203
+ }
204
+ const itemTypes = value.map((item, idx) => this.inferType(item, [...path, `[${idx}]`]));
205
+ if (this.isIdValuePattern(value)) {
206
+ const firstItem = value[0];
207
+ const valueType = this.inferType(firstItem.value, [...path, "[0].value"]);
208
+ const idValueTypeName = `${this.prefix}IdValue`;
209
+ this.idValueTypes.add(idValueTypeName);
210
+ const typeString = valueType.type !== "string" ? `${idValueTypeName}<${valueType.type}>` : idValueTypeName;
211
+ return {
212
+ type: `${typeString}[]`,
213
+ isOptional: false,
214
+ isArray: true,
215
+ nestedTypes: /* @__PURE__ */ new Map()
216
+ };
217
+ }
218
+ const firstType = itemTypes[0];
219
+ const allSameType = itemTypes.every(
220
+ (t) => t.type === firstType.type && !t.isArray && t.nestedTypes.size === firstType.nestedTypes.size
221
+ );
222
+ if (allSameType && !firstType.isArray) {
223
+ return {
224
+ type: `${firstType.type}[]`,
225
+ isOptional: false,
226
+ isArray: true,
227
+ nestedTypes: firstType.nestedTypes
228
+ };
229
+ }
230
+ const uniqueTypes = Array.from(new Set(itemTypes.map((t) => t.type)));
231
+ const unionType = uniqueTypes.length === 1 ? uniqueTypes[0] : uniqueTypes.join(" | ");
232
+ return {
233
+ type: `${unionType}[]`,
234
+ isOptional: false,
235
+ isArray: true,
236
+ nestedTypes: /* @__PURE__ */ new Map()
237
+ };
238
+ }
239
+ if (typeof value === "object") {
240
+ const obj = value;
241
+ const typeName = this.generateTypeName(path);
242
+ const nestedTypes = /* @__PURE__ */ new Map();
243
+ for (const [key, val] of Object.entries(obj)) {
244
+ nestedTypes.set(key, this.inferType(val, [...path, key]));
245
+ }
246
+ if (!this.generatedTypes.has(typeName)) {
247
+ this.generatedTypes.add(typeName);
248
+ this.typeMap.set(typeName, this.generateInterface(typeName, nestedTypes, obj));
249
+ }
250
+ return {
251
+ type: typeName,
252
+ isOptional: false,
253
+ isArray: false,
254
+ nestedTypes
255
+ };
256
+ }
257
+ const jsType = typeof value;
258
+ let tsType;
259
+ if (jsType === "number") {
260
+ tsType = "number";
261
+ } else if (jsType === "boolean") {
262
+ tsType = "boolean";
263
+ } else {
264
+ tsType = "string";
265
+ }
266
+ return { type: tsType, isOptional: false, isArray: false, nestedTypes: /* @__PURE__ */ new Map() };
267
+ }
268
+ /**
269
+ * Check if an array follows the AlgoliaIdValue pattern
270
+ */
271
+ isIdValuePattern(arr) {
272
+ return arr.length > 0 && arr.every(
273
+ (item) => typeof item === "object" && item !== null && "id" in item && "value" in item && typeof item.id === "string"
274
+ );
275
+ }
276
+ /**
277
+ * Generate a TypeScript type name from a path
278
+ */
279
+ generateTypeName(path) {
280
+ if (path.length === 0) {
281
+ return `${this.prefix}Hit${this.postfix}`;
282
+ }
283
+ const lastPart = path[path.length - 1];
284
+ const parts = lastPart.replace(/[\[\]]/g, "").split(/[-_\s]+/).filter(Boolean);
285
+ const pascalCase = parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
286
+ if (path.length > 1) {
287
+ const parent = path[path.length - 2];
288
+ if (parent.includes("Info")) {
289
+ return `${this.prefix}${pascalCase}Info${this.postfix}`;
290
+ }
291
+ }
292
+ return `${this.prefix}${pascalCase}${this.postfix}`;
293
+ }
294
+ /**
295
+ * Generate TypeScript interface code
296
+ */
297
+ generateInterface(typeName, nestedTypes, sampleObj) {
298
+ const lines = [];
299
+ const description = this.getTypeDescription(typeName);
300
+ lines.push("/**");
301
+ lines.push(` * ${description}`);
302
+ lines.push(" */");
303
+ lines.push(`export interface ${typeName} {`);
304
+ const sortedKeys = Array.from(nestedTypes.keys()).sort();
305
+ for (const key of sortedKeys) {
306
+ const typeInfo = nestedTypes.get(key);
307
+ const value = sampleObj[key];
308
+ const isOptional = typeInfo.isOptional || value === null || value === void 0;
309
+ const optionalMarker = isOptional ? "?" : "";
310
+ let typeString = typeInfo.type;
311
+ if (typeInfo.isArray && !typeString.endsWith("[]")) {
312
+ typeString = `${typeString}[]`;
313
+ }
314
+ if (value === null && typeString !== "null") {
315
+ typeString = `${typeString} | null`;
316
+ }
317
+ lines.push(` ${key}${optionalMarker}: ${typeString};`);
318
+ }
319
+ lines.push("}");
320
+ lines.push("");
321
+ return lines.join("\n");
322
+ }
323
+ /**
324
+ * Get a description for a type based on its name
325
+ */
326
+ getTypeDescription(typeName) {
327
+ const withoutPrefix = typeName.replace(new RegExp(`^${this.prefix}`), "");
328
+ const withoutPostfix = withoutPrefix.replace(new RegExp(`${this.postfix}$`), "");
329
+ const readable = withoutPostfix.replace(/([A-Z])/g, " $1").trim().toLowerCase();
330
+ return readable.charAt(0).toUpperCase() + readable.slice(1) + " structure in Algolia";
331
+ }
332
+ /**
333
+ * Collect all types that need to be generated
334
+ */
335
+ collectTypes(typeInfo, typesToGenerate) {
336
+ if (typeInfo.nestedTypes.size > 0 && !typeInfo.isArray) {
337
+ typesToGenerate.add(typeInfo.type);
338
+ for (const nestedTypeInfo of typeInfo.nestedTypes.values()) {
339
+ this.collectTypes(nestedTypeInfo, typesToGenerate);
340
+ }
341
+ }
342
+ }
343
+ /**
344
+ * Get dependencies for a type (types that it references)
345
+ */
346
+ getTypeDependencies(typeName) {
347
+ const typeCode = this.typeMap.get(typeName);
348
+ if (!typeCode) return [];
349
+ const dependencies = [];
350
+ const importRegex = new RegExp(`(${this.prefix}\\w+)`, "g");
351
+ const matches = typeCode.matchAll(importRegex);
352
+ for (const match of matches) {
353
+ const depTypeName = match[1];
354
+ if (depTypeName !== typeName && this.generatedTypes.has(depTypeName)) {
355
+ dependencies.push(depTypeName);
356
+ }
357
+ }
358
+ return Array.from(new Set(dependencies));
359
+ }
360
+ /**
361
+ * Sort types by dependencies (topological sort)
362
+ */
363
+ sortTypesByDependencies(typeNames) {
364
+ const sorted = [];
365
+ const visited = /* @__PURE__ */ new Set();
366
+ const visiting = /* @__PURE__ */ new Set();
367
+ const visit = (typeName) => {
368
+ if (visiting.has(typeName)) {
369
+ return;
370
+ }
371
+ if (visited.has(typeName)) {
372
+ return;
373
+ }
374
+ visiting.add(typeName);
375
+ const dependencies = this.getTypeDependencies(typeName);
376
+ for (const dep of dependencies) {
377
+ visit(dep);
378
+ }
379
+ visiting.delete(typeName);
380
+ visited.add(typeName);
381
+ sorted.push(typeName);
382
+ };
383
+ for (const typeName of typeNames) {
384
+ visit(typeName);
385
+ }
386
+ return sorted;
387
+ }
388
+ /**
389
+ * Generate IdValue type definition (generic type)
390
+ */
391
+ generateIdValueType(typeName) {
392
+ return `export type ${typeName}<T = string> = {
393
+ id: string;
394
+ value: T;
395
+ };
396
+
397
+ `;
398
+ }
399
+ /**
400
+ * Generate all types as a single file content
401
+ */
402
+ generateAllTypes(sampleHit) {
403
+ const rootType = this.inferType(sampleHit, []);
404
+ const typesToGenerate = /* @__PURE__ */ new Set();
405
+ this.collectTypes(rootType, typesToGenerate);
406
+ const rootTypeName = rootType.type;
407
+ if (rootType.nestedTypes.size > 0 && !rootType.isArray) {
408
+ typesToGenerate.add(rootTypeName);
409
+ }
410
+ const sortedTypes = this.sortTypesByDependencies(Array.from(typesToGenerate));
411
+ const lines = [];
412
+ lines.push("/**");
413
+ lines.push(` * Generated TypeScript types for Algolia index: ${this.indexName}`);
414
+ lines.push(" * This file is auto-generated. Do not edit manually.");
415
+ lines.push(" */");
416
+ lines.push("");
417
+ if (this.idValueTypes.size > 0) {
418
+ const idValueTypeName = Array.from(this.idValueTypes)[0];
419
+ lines.push(this.generateIdValueType(idValueTypeName));
420
+ }
421
+ for (const typeName of sortedTypes) {
422
+ const typeCode = this.typeMap.get(typeName);
423
+ if (typeCode) {
424
+ lines.push(typeCode);
425
+ }
426
+ }
427
+ return lines.join("\n");
428
+ }
429
+ };
430
+ function generateTypeScriptTypes(sampleHit, config) {
431
+ const generator = new TypeGenerator(config);
432
+ return generator.generateAllTypes(sampleHit);
433
+ }
434
+
435
+ // src/utils/fetch-algolia-data.ts
436
+ async function fetchAlgoliaData(filePath, generatorConfig, overwrite) {
437
+ console.log(`
438
+ Processing file: ${filePath}`);
439
+ const resolvedPath = resolve2(process.cwd(), filePath);
440
+ if (existsSync2(resolvedPath) && !overwrite) {
441
+ throw new Error(
442
+ `File already exists: ${resolvedPath}
443
+ Set overwrite: true in config to allow overwriting existing files.`
444
+ );
445
+ }
446
+ console.log(`Connecting to Algolia...`);
447
+ console.log(`App ID: ${generatorConfig.appId}`);
448
+ let client;
449
+ try {
450
+ client = algoliasearch(
451
+ generatorConfig.appId,
452
+ generatorConfig.searchKey
453
+ );
454
+ } catch (error) {
455
+ throw new Error(
456
+ `Failed to initialize Algolia client: ${error instanceof Error ? error.message : String(error)}`
457
+ );
458
+ }
459
+ console.log(`Fetching sample record from index: ${generatorConfig.indexName}`);
460
+ let results;
461
+ try {
462
+ results = await client.search([
463
+ {
464
+ indexName: generatorConfig.indexName,
465
+ query: "",
466
+ params: {
467
+ hitsPerPage: 1
468
+ }
469
+ }
470
+ ]);
471
+ } catch (error) {
472
+ let errorMessage;
473
+ if (error instanceof Error) {
474
+ errorMessage = error.message;
475
+ } else if (error && typeof error === "object") {
476
+ const errorObj = error;
477
+ if (errorObj.message) {
478
+ errorMessage = String(errorObj.message);
479
+ } else if (errorObj.status) {
480
+ errorMessage = `HTTP ${errorObj.status}: ${errorObj.statusText || "Unknown error"}`;
481
+ } else {
482
+ try {
483
+ errorMessage = JSON.stringify(error, null, 2);
484
+ } catch {
485
+ errorMessage = String(error);
486
+ }
487
+ }
488
+ } else {
489
+ errorMessage = String(error);
490
+ }
491
+ throw new Error(
492
+ `Failed to fetch data from Algolia index "${generatorConfig.indexName}" (App ID: ${generatorConfig.appId}): ${errorMessage}`
493
+ );
494
+ }
495
+ if (!results.results || results.results.length === 0) {
496
+ throw new Error(`No results found in Algolia index: ${generatorConfig.indexName}`);
497
+ }
498
+ const result = results.results[0];
499
+ if (!("hits" in result) || !result.hits || result.hits.length === 0) {
500
+ throw new Error(`No hits found in Algolia index: ${generatorConfig.indexName}`);
501
+ }
502
+ const sampleHit = result.hits[0];
503
+ console.log("Sample record fetched successfully");
504
+ console.log(`ObjectID: ${sampleHit.objectID || "N/A"}`);
505
+ const fileContent = generateTypeScriptTypes(sampleHit, generatorConfig);
506
+ const dir = dirname(resolvedPath);
507
+ if (!existsSync2(dir)) {
508
+ mkdirSync(dir, { recursive: true });
509
+ }
510
+ writeFileSync(resolvedPath, fileContent, "utf-8");
511
+ console.log(`Generated file: ${filePath}`);
512
+ }
513
+
514
+ // src/index.ts
515
+ var main = async (configPath) => {
516
+ try {
517
+ const config = await loadConfig(configPath);
518
+ console.log("Config loaded successfully");
519
+ const generatesArray = Array.isArray(config.generates) ? config.generates : [config.generates];
520
+ for (const urlSchema of generatesArray) {
521
+ for (const [filePath, generatorConfig] of Object.entries(urlSchema)) {
522
+ try {
523
+ await fetchAlgoliaData(filePath, generatorConfig, config.overwrite);
524
+ } catch (error) {
525
+ console.error(`
526
+ Error processing file: ${filePath}`);
527
+ if (error instanceof Error) {
528
+ console.error(error.message);
529
+ if (error.stack) {
530
+ console.error(error.stack);
531
+ }
532
+ } else {
533
+ try {
534
+ console.error(JSON.stringify(error, null, 2));
535
+ } catch {
536
+ console.error(String(error));
537
+ }
538
+ }
539
+ }
540
+ }
541
+ }
542
+ } catch (error) {
543
+ console.error("Error loading config:");
544
+ if (error instanceof Error) {
545
+ console.error(error.message);
546
+ } else {
547
+ console.error(String(error));
548
+ }
549
+ process.exit(1);
550
+ }
551
+ };
552
+
553
+ export {
554
+ validateGeneratorConfig,
555
+ validateUrlSchema,
556
+ validateConfig,
557
+ main
558
+ };