stabilize-orm 1.0.7 → 1.0.9

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,594 @@
1
+ #!/usr/bin/env bun
2
+ import { program } from "commander";
3
+ import {
4
+ generateMigration,
5
+ Stabilize,
6
+ DBType,
7
+ type DBConfig,
8
+ LogLevel,
9
+ type LoggerConfig,
10
+ runMigrations,
11
+ } from '../lib';
12
+ import * as fs from "fs/promises";
13
+ import * as path from "path";
14
+ import { glob } from "glob";
15
+
16
+ // --- ANSI Color and Styling Helpers ---
17
+ const C = {
18
+ RESET: "\x1b[0m",
19
+ BRIGHT: "\x1b[1m",
20
+ DIM: "\x1b[2m",
21
+ RED: "\x1b[31m",
22
+ GREEN: "\x1b[32m",
23
+ YELLOW: "\x1b[33m",
24
+ BLUE: "\x1b[34m",
25
+ MAGENTA: "\x1b[35m",
26
+ CYAN: "\x1b[36m",
27
+ WHITE: "\x1b[37m",
28
+ BG_GREEN: "\x1b[42m\x1b[30m", // Black text on Green background
29
+ BG_RED: "\x1b[41m\x1b[37m", // White text on Red background
30
+ BG_YELLOW: "\x1b[43m\x1b[30m",// Black text on Yellow background
31
+ };
32
+
33
+
34
+ program.version("1.0.5").description("Stabilize ORM CLI");
35
+
36
+ // Helper function to load configuration
37
+ async function loadConfig(configPath: string): Promise<{ config: DBConfig, loggerConfig: LoggerConfig, orm: Stabilize }> {
38
+ const absoluteConfigPath = path.resolve(process.cwd(), configPath);
39
+ const configModule = await import(absoluteConfigPath);
40
+ const config: DBConfig = configModule.default || configModule;
41
+
42
+ const logLevel = program.opts().logLevel as LogLevel;
43
+
44
+ const loggerConfig: LoggerConfig = {
45
+ level: logLevel,
46
+ filePath: path.resolve(process.cwd(), "logs/stabilize.log"),
47
+ maxFileSize: 5 * 1024 * 1024,
48
+ maxFiles: 3,
49
+ };
50
+
51
+ const orm = new Stabilize(
52
+ config,
53
+ { enabled: false, ttl: 60 },
54
+ loggerConfig,
55
+ );
56
+
57
+ return { config, loggerConfig, orm };
58
+ }
59
+
60
+ // --------------------------------------------------------------------------------------------------
61
+ // COMMAND: GENERATE
62
+ // --------------------------------------------------------------------------------------------------
63
+
64
+ program
65
+ .command("generate <type> <name>")
66
+ .description("Generate a model, migration, or seed file")
67
+ .option(
68
+ "-l, --log-level <level>",
69
+ "Log level (error, warn, info, debug)",
70
+ "info",
71
+ )
72
+ .action(async (type: string, name: string) => {
73
+ try {
74
+ if (type === "migration") {
75
+ const modelPath = path.resolve(process.cwd(), "models", `${name}.ts`);
76
+ const modelModule = await import(modelPath);
77
+ const model = Object.values(modelModule)[0] as new (
78
+ ...args: any[]
79
+ ) => any;
80
+ const migration = await generateMigration(
81
+ model,
82
+ `create_${name.toLowerCase()}`,
83
+ );
84
+ const migrationDir = path.resolve(process.cwd(), "migrations");
85
+ await fs.mkdir(migrationDir, { recursive: true });
86
+ const timestamp = new Date().toISOString().replace(/[-:T.]/g, "");
87
+ const migrationFile = path.join(
88
+ migrationDir,
89
+ `${timestamp}_${name.toLowerCase()}.ts`,
90
+ );
91
+ await fs.writeFile(
92
+ migrationFile,
93
+ `import { Migration } from 'stabilize-orm/src/types';\n\nconst migration: Migration = ${JSON.stringify(migration, null, 2)};\n\nexport default migration;`,
94
+ );
95
+ console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Migration generated: ${C.GREEN}${migrationFile}${C.RESET}`);
96
+
97
+ } else if (type === "model") {
98
+ const modelDir = path.resolve(process.cwd(), "models");
99
+ await fs.mkdir(modelDir, { recursive: true });
100
+ const modelFile = path.join(modelDir, `${name}.ts`);
101
+ const modelContent = `
102
+ import { Model, Column, Required } from 'stabilize-orm';
103
+
104
+ @Model('${name.toLowerCase()}s')
105
+ export class ${name} {
106
+ @Column('id', 'INTEGER')
107
+ id?: number;
108
+
109
+ @Column('name', 'TEXT')
110
+ @Required()
111
+ name: string;
112
+ }
113
+ `;
114
+ await fs.writeFile(modelFile, modelContent);
115
+ console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Model generated: ${C.GREEN}${modelFile}${C.RESET}`);
116
+
117
+ } else if (type === "seed") {
118
+ const seedDir = path.resolve(process.cwd(), "seeds");
119
+ await fs.mkdir(seedDir, { recursive: true });
120
+ const timestamp = new Date().toISOString().replace(/[-:T.]/g, "");
121
+ const seedFile = path.join(
122
+ seedDir,
123
+ `${timestamp}_${name.toLowerCase()}.ts`,
124
+ );
125
+ const seedContent = `
126
+ import { Stabilize } from 'stabilize-orm';
127
+ import { ${name} } from '../models/${name}'; // Assuming model path
128
+
129
+ export const dependencies = [];
130
+
131
+ export async function seed(orm: Stabilize) {
132
+ const repo = orm.getRepository(${name});
133
+ await repo.bulkCreate([
134
+ { name: '${name} 1' },
135
+ { name: '${name} 2' },
136
+ ], { batchSize: 100 });
137
+
138
+ await orm['client'].query(
139
+ \`INSERT INTO seed_history (name, applied_at) VALUES (?, ?)\`,
140
+ ['${timestamp}_${name.toLowerCase()}', new Date().toISOString()]
141
+ );
142
+ }
143
+
144
+ export async function rollback(orm: Stabilize) {
145
+ const repo = orm.getRepository(${name});
146
+ // NOTE: This rollback logic assumes your entities have an 'id' field for deletion.
147
+ const entities = await repo.find().execute(orm['client']);
148
+ await repo.bulkDelete(entities.map(e => e.id!), { batchSize: 100 });
149
+
150
+ await orm['client'].query(
151
+ \`DELETE FROM seed_history WHERE name = ?\`,
152
+ ['${timestamp}_${name.toLowerCase()}']
153
+ );
154
+ }
155
+ `;
156
+ await fs.writeFile(seedFile, seedContent);
157
+ console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Seed generated: ${C.GREEN}${seedFile}${C.RESET}`);
158
+ } else {
159
+ console.error(`${C.BG_RED} ERROR ${C.RESET} Invalid type. Use ${C.YELLOW}"model", "migration", or "seed"${C.RESET}.`);
160
+ }
161
+ } catch (error) {
162
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Error generating file:`, error);
163
+ }
164
+ });
165
+
166
+
167
+ // --------------------------------------------------------------------------------------------------
168
+ // COMMAND: MIGRATE
169
+ // --------------------------------------------------------------------------------------------------
170
+
171
+ program
172
+ .command("migrate")
173
+ .description("Apply all pending migrations")
174
+ .option(
175
+ "-c, --config <path>",
176
+ "Path to database config file",
177
+ "config/database.ts",
178
+ )
179
+ .option(
180
+ "-l, --log-level <level>",
181
+ "Log level (error, warn, info, debug)",
182
+ "info",
183
+ )
184
+ .action(async (options) => {
185
+ let orm: Stabilize | null = null;
186
+ try {
187
+ const { config } = await loadConfig(options.config);
188
+
189
+ // Re-initialize ORM with correct logging configuration
190
+ orm = new Stabilize(config, { enabled: false, ttl: 60 }, { level: options.logLevel as LogLevel });
191
+
192
+ const migrationDir = path.resolve(process.cwd(), "migrations");
193
+ const migrationFiles = await glob(`${migrationDir}/*.ts`);
194
+
195
+ const migrations = [];
196
+ for (const file of migrationFiles.sort()) {
197
+ const migrationModule = await import(file);
198
+ // Assuming migration files export a default object with { up: string[], down: string[] }
199
+ migrations.push(migrationModule.default || migrationModule);
200
+ }
201
+
202
+ if (migrations.length === 0) {
203
+ console.log(`${C.YELLOW} WARNING ${C.RESET} No migration files found in migrations/ directory.`);
204
+ await orm.close();
205
+ return;
206
+ }
207
+
208
+ console.log(`${C.BLUE} INFO ${C.RESET} Running ${C.BRIGHT}${migrations.length}${C.RESET} migration files...`);
209
+
210
+ // runMigrations handles applying only the unapplied ones.
211
+ await runMigrations(config, migrations);
212
+
213
+ console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Migrations completed successfully.`);
214
+ await orm.close();
215
+
216
+ } catch (error) {
217
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Migration failed:`, error);
218
+ if (orm) await orm.close();
219
+ process.exit(1);
220
+ }
221
+ });
222
+
223
+
224
+ // --------------------------------------------------------------------------------------------------
225
+ // COMMAND: MIGRATE:ROLLBACK (One step back)
226
+ // --------------------------------------------------------------------------------------------------
227
+
228
+ program
229
+ .command("migrate:rollback")
230
+ .description("Rollback the most recently applied migration")
231
+ .option(
232
+ "-c, --config <path>",
233
+ "Path to database config file",
234
+ "config/database.ts",
235
+ )
236
+ .option(
237
+ "-l, --log-level <level>",
238
+ "Log level (error, warn, info, debug)",
239
+ "info",
240
+ )
241
+ .action(async (options) => {
242
+ let orm: Stabilize | null = null;
243
+ try {
244
+ const { config } = await loadConfig(options.config);
245
+ orm = new Stabilize(config, { enabled: false, ttl: 60 }, { level: options.logLevel as LogLevel });
246
+
247
+ // 1. Get the last applied migration record from the DB
248
+ const latestApplied = await orm["client"].query<{ name: string }>(
249
+ `SELECT name FROM migrations ORDER BY applied_at DESC LIMIT 1`,
250
+ );
251
+
252
+ if (latestApplied.length === 0) {
253
+ console.log(`${C.YELLOW} WARNING ${C.RESET} No migrations to rollback.`);
254
+ await orm.close();
255
+ return;
256
+ }
257
+
258
+ const migrationName = latestApplied[0]!.name;
259
+ const migrationDir = path.resolve(process.cwd(), "migrations");
260
+ const migrationFiles = await glob(`${migrationDir}/*.ts`);
261
+
262
+ // 2. Find the corresponding file
263
+ const migrationFile = migrationFiles.find(f => path.basename(f, '.ts') === migrationName);
264
+
265
+ if (!migrationFile) {
266
+ console.error(`${C.BG_RED} ERROR ${C.RESET} Migration file for ${migrationName} not found.`);
267
+ await orm.close();
268
+ return;
269
+ }
270
+
271
+ // 3. Load the rollback (down) query
272
+ const migrationModule = await import(migrationFile);
273
+ const migration = migrationModule.default || migrationModule;
274
+ const downQueries: string[] = migration.down;
275
+
276
+ if (!downQueries || downQueries.length === 0) {
277
+ console.error(`${C.BG_RED} ERROR ${C.RESET} Migration ${migrationName} does not contain a 'down' array for rollback.`);
278
+ await orm.close();
279
+ return;
280
+ }
281
+
282
+ console.log(`${C.BLUE} INFO ${C.RESET} Rolling back migration: ${C.YELLOW}${migrationName}${C.RESET}`);
283
+
284
+ // 4. Run the down queries inside a transaction (best practice)
285
+ await orm.transaction(async () => {
286
+ for (const query of downQueries) {
287
+ await orm!['client'].query(query, []);
288
+ }
289
+ // 5. Remove the migration record from the history table
290
+ await orm!["client"].query(
291
+ `DELETE FROM migrations WHERE name = ?`,
292
+ [migrationName],
293
+ );
294
+ });
295
+
296
+ console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Rolled back migration: ${C.GREEN}${migrationName}${C.RESET}`);
297
+ await orm.close();
298
+
299
+ } catch (error) {
300
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Migration rollback failed:`, error);
301
+ if (orm) await orm.close();
302
+ process.exit(1);
303
+ }
304
+ });
305
+
306
+
307
+ // --------------------------------------------------------------------------------------------------
308
+ // COMMAND: SEED
309
+ // --------------------------------------------------------------------------------------------------
310
+
311
+ program
312
+ .command("seed")
313
+ .description("Run seed files to populate the database")
314
+ .option(
315
+ "-c, --config <path>",
316
+ "Path to database config file",
317
+ "config/database.ts",
318
+ )
319
+ .option(
320
+ "-l, --log-level <level>",
321
+ "Log level (error, warn, info, debug)",
322
+ "info",
323
+ )
324
+ .action(async (options) => {
325
+ let orm: Stabilize | null = null;
326
+ try {
327
+ const { config } = await loadConfig(options.config);
328
+ orm = new Stabilize(config, { enabled: false, ttl: 60 }, { level: options.logLevel as LogLevel });
329
+
330
+ await orm["client"].query(`
331
+ CREATE TABLE IF NOT EXISTS seed_history (
332
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
333
+ name TEXT NOT NULL,
334
+ applied_at TEXT NOT NULL
335
+ )
336
+ `);
337
+
338
+ const seedDir = path.resolve(process.cwd(), "seeds");
339
+ const seedFiles = await glob(`${seedDir}/*.ts`);
340
+ if (seedFiles.length === 0) {
341
+ console.log(`${C.YELLOW} WARNING ${C.RESET} No seed files found in seeds/ directory.`);
342
+ await orm.close();
343
+ return;
344
+ }
345
+
346
+ const seedGraph = new Map<
347
+ string,
348
+ { file: string; dependencies: string[] }
349
+ >();
350
+ for (const file of seedFiles) {
351
+ const seedName = path.basename(file, ".ts");
352
+ const seedModule = await import(file);
353
+ seedGraph.set(seedName, {
354
+ file,
355
+ dependencies: seedModule.dependencies || [],
356
+ });
357
+ }
358
+
359
+ const orderedSeeds = topologicalSort(seedGraph);
360
+
361
+ console.log(`${C.BLUE} INFO ${C.RESET} Running ${C.BRIGHT}${orderedSeeds.length}${C.RESET} seed files (sorted by dependency)...`);
362
+ let seedsApplied = 0;
363
+ for (const seedName of orderedSeeds) {
364
+ const { file } = seedGraph.get(seedName)!;
365
+ const applied = await orm["client"].query<{ id: number }>(
366
+ `SELECT id FROM seed_history WHERE name = ?`,
367
+ [seedName],
368
+ );
369
+ if (applied.length > 0) {
370
+ console.log(` ${C.DIM}Skipping already applied seed:${C.RESET} ${seedName}`);
371
+ continue;
372
+ }
373
+
374
+ console.log(` ${C.BRIGHT}Executing seed:${C.RESET} ${C.MAGENTA}${seedName}${C.RESET}`);
375
+ const seedModule = await import(file);
376
+ const seedFn = seedModule.default || seedModule.seed;
377
+ if (typeof seedFn === "function") {
378
+ await seedFn(orm);
379
+ seedsApplied++;
380
+ } else {
381
+ console.error(
382
+ `${C.BG_RED} ERROR ${C.RESET} Seed file ${file} must export a default function or a function named 'seed'.`,
383
+ );
384
+ }
385
+ }
386
+
387
+ console.log(`\n${C.BG_GREEN} SUCCESS ${C.RESET} Seeding completed. ${C.GREEN}${seedsApplied} new seeds applied.${C.RESET}`);
388
+ await orm.close();
389
+ } catch (error) {
390
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Seeding failed:`, error);
391
+ if (orm) await orm.close();
392
+ process.exit(1);
393
+ }
394
+ });
395
+
396
+ // --------------------------------------------------------------------------------------------------
397
+ // COMMAND: SEED:ROLLBACK (One step back)
398
+ // --------------------------------------------------------------------------------------------------
399
+
400
+ program
401
+ .command("seed:rollback")
402
+ .description("Rollback the most recently applied seed")
403
+ .option(
404
+ "-c, --config <path>",
405
+ "Path to database config file",
406
+ "config/database.ts",
407
+ )
408
+ .option(
409
+ "-l, --log-level <level>",
410
+ "Log level (error, warn, info, debug)",
411
+ "info",
412
+ )
413
+ .action(async (options) => {
414
+ let orm: Stabilize | null = null;
415
+ try {
416
+ const { config } = await loadConfig(options.config);
417
+ orm = new Stabilize(config, { enabled: false, ttl: 60 }, { level: options.logLevel as LogLevel });
418
+
419
+ const seedDir = path.resolve(process.cwd(), "seeds");
420
+ const seedFiles = await glob(`${seedDir}/*.ts`);
421
+ const seedGraph = new Map<
422
+ string,
423
+ { file: string; dependencies: string[] }
424
+ >();
425
+ for (const file of seedFiles) {
426
+ const seedName = path.basename(file, ".ts");
427
+ const seedModule = await import(file);
428
+ seedGraph.set(seedName, {
429
+ file,
430
+ dependencies: seedModule.dependencies || [],
431
+ });
432
+ }
433
+
434
+ const latestSeed = await orm["client"].query<{ name: string }>(
435
+ `SELECT name FROM seed_history ORDER BY applied_at DESC LIMIT 1`,
436
+ );
437
+
438
+ if (latestSeed.length === 0) {
439
+ console.log(`${C.YELLOW} WARNING ${C.RESET} No seeds to rollback.`);
440
+ await orm.close();
441
+ return;
442
+ }
443
+
444
+ const seedName = latestSeed[0]!.name;
445
+ const seedFile = seedGraph.get(seedName)?.file;
446
+ if (!seedFile) {
447
+ console.error(`${C.BG_RED} ERROR ${C.RESET} Seed file for ${seedName} not found.`);
448
+ await orm.close();
449
+ return;
450
+ }
451
+
452
+ console.log(`${C.BLUE} INFO ${C.RESET} Rolling back seed: ${C.YELLOW}${seedName}${C.RESET}`);
453
+ const seedModule = await import(seedFile);
454
+ const rollbackFn = seedModule.rollback;
455
+
456
+ if (typeof rollbackFn === "function") {
457
+ await rollbackFn(orm);
458
+ console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Rolled back seed: ${C.GREEN}${seedName}${C.RESET}`);
459
+ } else {
460
+ console.error(
461
+ `${C.BG_RED} ERROR ${C.RESET} Seed file ${seedFile} must export a 'rollback' function.`,
462
+ );
463
+ }
464
+
465
+ await orm.close();
466
+ } catch (error) {
467
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Seed rollback failed:`, error);
468
+ if (orm) await orm.close();
469
+ process.exit(1);
470
+ }
471
+ });
472
+
473
+
474
+ // --------------------------------------------------------------------------------------------------
475
+ // COMMAND: STATUS
476
+ // --------------------------------------------------------------------------------------------------
477
+
478
+ program
479
+ .command("status")
480
+ .description("Display status of migrations and seeds")
481
+ .option(
482
+ "-c, --config <path>",
483
+ "Path to database config file",
484
+ "config/database.ts",
485
+ )
486
+ .option(
487
+ "-l, --log-level <level>",
488
+ "Log level (error, warn, info, debug)",
489
+ "info",
490
+ )
491
+ .action(async (options) => {
492
+ let orm: Stabilize | null = null;
493
+ try {
494
+ const { config } = await loadConfig(options.config);
495
+ orm = new Stabilize(config, { enabled: false, ttl: 60 }, { level: options.logLevel as LogLevel });
496
+
497
+ const migrationDir = path.resolve(process.cwd(), "migrations");
498
+ const migrationFiles = await glob(`${migrationDir}/*.ts`);
499
+
500
+ // Ensure migrations table exists for query purposes
501
+ await orm["client"].query(`
502
+ CREATE TABLE IF NOT EXISTS migrations (
503
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
504
+ name TEXT NOT NULL,
505
+ applied_at TEXT NOT NULL
506
+ )
507
+ `);
508
+
509
+ console.log(`\n${C.BRIGHT}Migration Status:${C.RESET}`);
510
+ console.log(`---------------------------------`);
511
+ for (const file of migrationFiles.sort()) {
512
+ const migrationName = path.basename(file, ".ts");
513
+ const applied = await orm["client"].query<{ name: string }>(
514
+ `SELECT name FROM migrations WHERE name = ?`,
515
+ [migrationName],
516
+ );
517
+ const status = applied.length > 0
518
+ ? `${C.BG_GREEN} APPLIED ${C.RESET}`
519
+ : `${C.BG_YELLOW} PENDING ${C.RESET}`;
520
+
521
+ console.log(`${status} ${C.WHITE}${migrationName}${C.RESET}`);
522
+ }
523
+
524
+ // Ensure seed_history table exists for query purposes
525
+ await orm["client"].query(`
526
+ CREATE TABLE IF NOT EXISTS seed_history (
527
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
528
+ name TEXT NOT NULL,
529
+ applied_at TEXT NOT NULL
530
+ )
531
+ `);
532
+
533
+ const seedDir = path.resolve(process.cwd(), "seeds");
534
+ const seedFiles = await glob(`${seedDir}/*.ts`);
535
+ console.log(`\n${C.BRIGHT}Seed Status:${C.RESET}`);
536
+ console.log(`---------------------------------`);
537
+ for (const file of seedFiles.sort()) {
538
+ const seedName = path.basename(file, ".ts");
539
+ const applied = await orm["client"].query<{ name: string }>(
540
+ `SELECT name FROM seed_history WHERE name = ?`,
541
+ [seedName],
542
+ );
543
+ const status = applied.length > 0
544
+ ? `${C.BG_GREEN} APPLIED ${C.RESET}`
545
+ : `${C.BG_YELLOW} PENDING ${C.RESET}`;
546
+
547
+ console.log(`${status} ${C.WHITE}${seedName}${C.RESET}`);
548
+ }
549
+
550
+ await orm.close();
551
+ } catch (error) {
552
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Status check failed:`, error);
553
+ if (orm) await orm.close();
554
+ process.exit(1);
555
+ }
556
+ });
557
+
558
+
559
+ // --------------------------------------------------------------------------------------------------
560
+ // TOPOLOGICAL SORT HELPER
561
+ // --------------------------------------------------------------------------------------------------
562
+
563
+ function topologicalSort(
564
+ graph: Map<string, { file: string; dependencies: string[] }>,
565
+ ): string[] {
566
+ const result: string[] = [];
567
+ const visited = new Set<string>();
568
+ const temp = new Set<string>();
569
+
570
+ function visit(node: string) {
571
+ if (temp.has(node))
572
+ throw new Error(`Circular dependency detected at ${node}`);
573
+ if (!visited.has(node)) {
574
+ temp.add(node);
575
+ const { dependencies } = graph.get(node)!;
576
+ for (const dep of dependencies) {
577
+ if (!graph.has(dep))
578
+ throw new Error(`Dependency ${dep} not found for ${node}`);
579
+ visit(dep);
580
+ }
581
+ temp.delete(node);
582
+ visited.add(node);
583
+ result.push(node);
584
+ }
585
+ }
586
+
587
+ for (const node of graph.keys()) {
588
+ if (!visited.has(node)) visit(node);
589
+ }
590
+
591
+ return result;
592
+ }
593
+
594
+ program.parse(process.argv);
package/lib/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ElectronSz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.