@nickchristensen/cliftin 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.
- package/README.md +206 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/exercises/list.d.ts +13 -0
- package/dist/commands/exercises/list.js +54 -0
- package/dist/commands/exercises/show.d.ts +21 -0
- package/dist/commands/exercises/show.js +140 -0
- package/dist/commands/programs/list.d.ts +6 -0
- package/dist/commands/programs/list.js +27 -0
- package/dist/commands/programs/show.d.ts +13 -0
- package/dist/commands/programs/show.js +71 -0
- package/dist/commands/workouts/list.d.ts +15 -0
- package/dist/commands/workouts/list.js +72 -0
- package/dist/commands/workouts/show.d.ts +9 -0
- package/dist/commands/workouts/show.js +72 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +24 -0
- package/dist/lib/db.d.ts +104 -0
- package/dist/lib/db.js +15 -0
- package/dist/lib/json-error.d.ts +5 -0
- package/dist/lib/json-error.js +6 -0
- package/dist/lib/json-weight.d.ts +7 -0
- package/dist/lib/json-weight.js +42 -0
- package/dist/lib/names.d.ts +5 -0
- package/dist/lib/names.js +70 -0
- package/dist/lib/output.d.ts +7 -0
- package/dist/lib/output.js +73 -0
- package/dist/lib/repositories/exercises.d.ts +30 -0
- package/dist/lib/repositories/exercises.js +280 -0
- package/dist/lib/repositories/programs.d.ts +6 -0
- package/dist/lib/repositories/programs.js +217 -0
- package/dist/lib/repositories/selectors.d.ts +3 -0
- package/dist/lib/repositories/selectors.js +35 -0
- package/dist/lib/repositories/workouts.d.ts +13 -0
- package/dist/lib/repositories/workouts.js +128 -0
- package/dist/lib/rpe.d.ts +2 -0
- package/dist/lib/rpe.js +4 -0
- package/dist/lib/time.d.ts +10 -0
- package/dist/lib/time.js +49 -0
- package/dist/lib/types.d.ts +123 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/units.d.ts +16 -0
- package/dist/lib/units.js +81 -0
- package/oclif.manifest.json +386 -0
- package/package.json +93 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class WorkoutsShow extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
workoutId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static enableJsonFlag: boolean;
|
|
8
|
+
run(): Promise<unknown | void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { format, isValid, parseISO } from 'date-fns';
|
|
3
|
+
import { closeDb, openDb } from '../../lib/db.js';
|
|
4
|
+
import { serializeWorkoutDetailWithWeightUnits } from '../../lib/json-weight.js';
|
|
5
|
+
import { renderTable } from '../../lib/output.js';
|
|
6
|
+
import { getWorkoutDetail } from '../../lib/repositories/workouts.js';
|
|
7
|
+
import { resolveGlobalWeightUnit, weightUnitLabel } from '../../lib/units.js';
|
|
8
|
+
function formatDurationMinutes(durationSeconds) {
|
|
9
|
+
if (durationSeconds === null)
|
|
10
|
+
return 'n/a';
|
|
11
|
+
return `${Math.round(durationSeconds / 60)} minutes`;
|
|
12
|
+
}
|
|
13
|
+
function formatWorkoutDate(dateIso) {
|
|
14
|
+
if (!dateIso)
|
|
15
|
+
return 'n/a';
|
|
16
|
+
const parsed = parseISO(dateIso);
|
|
17
|
+
if (!isValid(parsed))
|
|
18
|
+
return dateIso;
|
|
19
|
+
return format(parsed, 'yyyy-MM-dd HH:mm');
|
|
20
|
+
}
|
|
21
|
+
export default class WorkoutsShow extends Command {
|
|
22
|
+
static args = {
|
|
23
|
+
workoutId: Args.string({ description: 'workout id', required: true }),
|
|
24
|
+
};
|
|
25
|
+
static description = 'Show one workout with exercises and sets';
|
|
26
|
+
static enableJsonFlag = true;
|
|
27
|
+
async run() {
|
|
28
|
+
const { args } = await this.parse(WorkoutsShow);
|
|
29
|
+
const context = openDb();
|
|
30
|
+
try {
|
|
31
|
+
if (!/^\d+$/.test(args.workoutId)) {
|
|
32
|
+
throw new Error('Workout detail requires numeric workout id.');
|
|
33
|
+
}
|
|
34
|
+
const detail = await getWorkoutDetail(context.db, Number(args.workoutId));
|
|
35
|
+
const unitPreference = await resolveGlobalWeightUnit(context.db);
|
|
36
|
+
const unitLabel = weightUnitLabel(unitPreference);
|
|
37
|
+
if (this.jsonEnabled()) {
|
|
38
|
+
const serialized = serializeWorkoutDetailWithWeightUnits(detail, unitPreference);
|
|
39
|
+
const durationSeconds = serialized.duration ?? null;
|
|
40
|
+
return {
|
|
41
|
+
...serialized,
|
|
42
|
+
duration: {
|
|
43
|
+
unit: 'seconds',
|
|
44
|
+
value: durationSeconds,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
this.log(`[${detail.id}] ${detail.routine ?? 'Workout'}`);
|
|
49
|
+
this.log(`Program: ${detail.program ?? 'n/a'}`);
|
|
50
|
+
this.log(`Date: ${formatWorkoutDate(detail.date)}`);
|
|
51
|
+
this.log(`Duration: ${formatDurationMinutes(detail.duration)}`);
|
|
52
|
+
for (const exercise of detail.exercises) {
|
|
53
|
+
this.log('');
|
|
54
|
+
this.log(`[${exercise.exerciseId ?? exercise.exerciseResultId}] ${exercise.name ?? '(unnamed)'}`);
|
|
55
|
+
this.log(renderTable(exercise.sets.map((set) => ({
|
|
56
|
+
id: set.id,
|
|
57
|
+
reps: set.reps,
|
|
58
|
+
rpe: set.rpe,
|
|
59
|
+
timeSeconds: set.timeSeconds,
|
|
60
|
+
volume: set.volume,
|
|
61
|
+
weight: set.weight === null ? null : `${set.weight} ${unitLabel}`,
|
|
62
|
+
})))
|
|
63
|
+
.split('\n')
|
|
64
|
+
.map((line) => `${line}`)
|
|
65
|
+
.join('\n'));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await closeDb(context);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from '@oclif/core';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from '@oclif/core';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getDbPath(): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
import { accessSync, constants } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
let loaded = false;
|
|
5
|
+
function loadEnv() {
|
|
6
|
+
if (loaded)
|
|
7
|
+
return;
|
|
8
|
+
dotenv.config({ path: resolve(process.cwd(), '.env.local'), quiet: true });
|
|
9
|
+
loaded = true;
|
|
10
|
+
}
|
|
11
|
+
export function getDbPath() {
|
|
12
|
+
loadEnv();
|
|
13
|
+
const path = process.env.LIFTIN_DB_PATH;
|
|
14
|
+
if (!path) {
|
|
15
|
+
throw new Error('Missing LIFTIN_DB_PATH. Add it to .env.local, for example: LIFTIN_DB_PATH=/Users/nick/code/cliftin/data/BelloDataModel.sqlite');
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
accessSync(path, constants.R_OK);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error(`Database file is not readable at LIFTIN_DB_PATH=${path}`);
|
|
22
|
+
}
|
|
23
|
+
return path;
|
|
24
|
+
}
|
package/dist/lib/db.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { Kysely } from 'kysely';
|
|
3
|
+
export interface DatabaseSchema {
|
|
4
|
+
Z_12ROUTINES: {
|
|
5
|
+
Z_12EXERCISES: number;
|
|
6
|
+
Z_28ROUTINES: number;
|
|
7
|
+
Z_FOK_12EXERCISES: null | number;
|
|
8
|
+
};
|
|
9
|
+
ZEQUIPMENT2: {
|
|
10
|
+
Z_PK: number;
|
|
11
|
+
ZID: null | string;
|
|
12
|
+
ZMEASURMENTUNIT: null | string;
|
|
13
|
+
ZNAME: null | string;
|
|
14
|
+
};
|
|
15
|
+
ZEXERCISECONFIGURATION: {
|
|
16
|
+
Z_PK: number;
|
|
17
|
+
ZINFORMATION: null | number;
|
|
18
|
+
ZREPS: null | number;
|
|
19
|
+
ZSETS: null | number;
|
|
20
|
+
ZTIME: null | number;
|
|
21
|
+
ZWEIGHT: null | number;
|
|
22
|
+
};
|
|
23
|
+
ZEXERCISEINFORMATION: {
|
|
24
|
+
Z_PK: number;
|
|
25
|
+
ZDEFAULTPROGRESSMETRIC: null | string;
|
|
26
|
+
ZEQUIPMENT: null | number;
|
|
27
|
+
ZISUSERCREATED: null | number;
|
|
28
|
+
ZMUSCLES: null | string;
|
|
29
|
+
ZNAME: null | string;
|
|
30
|
+
ZPERCEPTIONSCALE: null | string;
|
|
31
|
+
ZSECONDARYMUSCLES: null | string;
|
|
32
|
+
ZSOFTDELETED: null | number;
|
|
33
|
+
ZSUPPORTSONEREPMAX: null | number;
|
|
34
|
+
ZTIMERBASED: null | number;
|
|
35
|
+
};
|
|
36
|
+
ZEXERCISERESULT: {
|
|
37
|
+
Z_FOK_WORKOUT: null | number;
|
|
38
|
+
Z_PK: number;
|
|
39
|
+
ZCONFIGURATION: null | number;
|
|
40
|
+
ZWORKOUT: null | number;
|
|
41
|
+
};
|
|
42
|
+
ZGYMSETRESULT: {
|
|
43
|
+
Z_FOK_EXERCISE: null | number;
|
|
44
|
+
Z_PK: number;
|
|
45
|
+
ZEXERCISE: null | number;
|
|
46
|
+
ZREPS: null | number;
|
|
47
|
+
ZRPE: null | number;
|
|
48
|
+
ZTIME: null | number;
|
|
49
|
+
ZVOLUME: null | number;
|
|
50
|
+
ZWEIGHT: null | number;
|
|
51
|
+
};
|
|
52
|
+
ZPERIOD: {
|
|
53
|
+
Z_FOK_WORKOUTPLAN: null | number;
|
|
54
|
+
Z_PK: number;
|
|
55
|
+
ZWORKOUTPLAN: null | number;
|
|
56
|
+
};
|
|
57
|
+
ZROUTINE: {
|
|
58
|
+
Z_FOK_PERIOD: null | number;
|
|
59
|
+
Z_PK: number;
|
|
60
|
+
ZNAME: null | string;
|
|
61
|
+
ZPERIOD: null | number;
|
|
62
|
+
ZSOFTDELETED: null | number;
|
|
63
|
+
ZWORKOUTPLAN: null | number;
|
|
64
|
+
};
|
|
65
|
+
ZSETCONFIGURATION: {
|
|
66
|
+
Z_PK: number;
|
|
67
|
+
ZEXERCISECONFIGURATION: null | number;
|
|
68
|
+
ZREPS: null | number;
|
|
69
|
+
ZRPE: null | number;
|
|
70
|
+
ZSETINDEX: null | number;
|
|
71
|
+
ZTIME: null | number;
|
|
72
|
+
ZWEIGHT: null | number;
|
|
73
|
+
};
|
|
74
|
+
ZSETTINGS: {
|
|
75
|
+
ZMEASURMENTUNIT: null | string;
|
|
76
|
+
};
|
|
77
|
+
ZWORKOUTPLAN: {
|
|
78
|
+
Z_PK: number;
|
|
79
|
+
ZDATEADDED: null | number;
|
|
80
|
+
ZID: Buffer | null;
|
|
81
|
+
ZISCURRENT: null | number;
|
|
82
|
+
ZISTEMPLATE: null | number;
|
|
83
|
+
ZNAME: null | string;
|
|
84
|
+
ZSOFTDELETED: null | number;
|
|
85
|
+
};
|
|
86
|
+
ZWORKOUTPROGRAMSINFO: {
|
|
87
|
+
Z_PK: number;
|
|
88
|
+
ZSECONDARYWORKOUTPROGRAMID: Buffer | null;
|
|
89
|
+
ZSELECTEDWORKOUTPROGRAMID: Buffer | null;
|
|
90
|
+
};
|
|
91
|
+
ZWORKOUTRESULT: {
|
|
92
|
+
Z_PK: number;
|
|
93
|
+
ZDURATION: null | number;
|
|
94
|
+
ZROUTINE: null | number;
|
|
95
|
+
ZROUTINENAME: null | string;
|
|
96
|
+
ZSTARTDATE: null | number;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export type DbContext = {
|
|
100
|
+
db: Kysely<DatabaseSchema>;
|
|
101
|
+
sqlite: Database.Database;
|
|
102
|
+
};
|
|
103
|
+
export declare function openDb(): DbContext;
|
|
104
|
+
export declare function closeDb(context: DbContext): Promise<void>;
|
package/dist/lib/db.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { Kysely, SqliteDialect } from 'kysely';
|
|
3
|
+
import { getDbPath } from './config.js';
|
|
4
|
+
export function openDb() {
|
|
5
|
+
const path = getDbPath();
|
|
6
|
+
const sqlite = new Database(path, { fileMustExist: true, readonly: true });
|
|
7
|
+
const db = new Kysely({
|
|
8
|
+
dialect: new SqliteDialect({ database: sqlite }),
|
|
9
|
+
});
|
|
10
|
+
return { db, sqlite };
|
|
11
|
+
}
|
|
12
|
+
export async function closeDb(context) {
|
|
13
|
+
await context.db.destroy();
|
|
14
|
+
context.sqlite.close();
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ExerciseHistoryRow, ProgramDetailTree, WorkoutDetail } from './types.js';
|
|
2
|
+
import { UnitPreference, withWeightUnit } from './units.js';
|
|
3
|
+
export declare function serializeExerciseHistoryRowsWithWeightUnits(rows: ExerciseHistoryRow[], unitPreference: UnitPreference): Array<Omit<ExerciseHistoryRow, 'topWeight'> & {
|
|
4
|
+
topWeight: ReturnType<typeof withWeightUnit>;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function serializeProgramDetailWithWeightUnits(detail: ProgramDetailTree, unitPreference: UnitPreference): unknown;
|
|
7
|
+
export declare function serializeWorkoutDetailWithWeightUnits(detail: WorkoutDetail, unitPreference: UnitPreference): unknown;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { withWeightUnit } from './units.js';
|
|
2
|
+
export function serializeExerciseHistoryRowsWithWeightUnits(rows, unitPreference) {
|
|
3
|
+
return rows.map((row) => ({
|
|
4
|
+
...row,
|
|
5
|
+
topWeight: withWeightUnit(row.topWeight, unitPreference),
|
|
6
|
+
}));
|
|
7
|
+
}
|
|
8
|
+
export function serializeProgramDetailWithWeightUnits(detail, unitPreference) {
|
|
9
|
+
return {
|
|
10
|
+
...detail,
|
|
11
|
+
weeks: detail.weeks.map((week) => ({
|
|
12
|
+
...week,
|
|
13
|
+
routines: week.routines.map((routine) => ({
|
|
14
|
+
...routine,
|
|
15
|
+
exercises: routine.exercises.map((exercise) => ({
|
|
16
|
+
...exercise,
|
|
17
|
+
plannedWeight: withWeightUnit(exercise.plannedWeight, unitPreference),
|
|
18
|
+
sets: exercise.sets.map((set) => ({
|
|
19
|
+
...set,
|
|
20
|
+
weight: withWeightUnit(set.weight, unitPreference),
|
|
21
|
+
})),
|
|
22
|
+
})),
|
|
23
|
+
})),
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function serializeWorkoutDetailWithWeightUnits(detail, unitPreference) {
|
|
28
|
+
return {
|
|
29
|
+
...detail,
|
|
30
|
+
exercises: detail.exercises.map((exercise) => ({
|
|
31
|
+
...exercise,
|
|
32
|
+
sets: exercise.sets.map((set) => ({
|
|
33
|
+
reps: set.reps,
|
|
34
|
+
rpe: set.rpe,
|
|
35
|
+
setId: set.id,
|
|
36
|
+
timeSeconds: set.timeSeconds,
|
|
37
|
+
volume: set.volume,
|
|
38
|
+
weight: withWeightUnit(set.weight, unitPreference),
|
|
39
|
+
})),
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function formatIdentifierTitleCase(value: string): string;
|
|
2
|
+
export declare function formatMuscleLabel(value: null | string): null | string;
|
|
3
|
+
export declare function formatEquipmentDisplayName(zName: null | string, zId: null | string): null | string;
|
|
4
|
+
export declare function formatExerciseName(name: string): string;
|
|
5
|
+
export declare function formatExerciseDisplayName(name: null | string, isUserCreated: boolean): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const PARENTHETICAL_SUFFIXES = new Set(['assisted', 'weighted']);
|
|
2
|
+
const UUID_LIKE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3
|
+
function toTitleWord(word) {
|
|
4
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
export function formatIdentifierTitleCase(value) {
|
|
7
|
+
const spaced = value
|
|
8
|
+
.replaceAll(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
9
|
+
.replaceAll(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
10
|
+
.replaceAll('_', ' ')
|
|
11
|
+
.replaceAll('-', ' ')
|
|
12
|
+
.replaceAll(/\s+/g, ' ')
|
|
13
|
+
.trim();
|
|
14
|
+
if (!spaced)
|
|
15
|
+
return '';
|
|
16
|
+
return spaced
|
|
17
|
+
.split(' ')
|
|
18
|
+
.map((word) => toTitleWord(word))
|
|
19
|
+
.join(' ');
|
|
20
|
+
}
|
|
21
|
+
export function formatMuscleLabel(value) {
|
|
22
|
+
if (!value)
|
|
23
|
+
return null;
|
|
24
|
+
const formatted = value
|
|
25
|
+
.split(/([^a-zA-Z0-9]+)/)
|
|
26
|
+
.map((part) => (/^[a-zA-Z0-9]+$/.test(part) ? formatIdentifierTitleCase(part) : part))
|
|
27
|
+
.join('');
|
|
28
|
+
return formatted.replaceAll(/\s*,\s*/g, ', ');
|
|
29
|
+
}
|
|
30
|
+
function isLocalizationKey(value) {
|
|
31
|
+
const normalized = value.trim().toLowerCase();
|
|
32
|
+
return normalized.includes(':') || normalized.endsWith('_name');
|
|
33
|
+
}
|
|
34
|
+
function isUuidLike(value) {
|
|
35
|
+
return UUID_LIKE.test(value.trim());
|
|
36
|
+
}
|
|
37
|
+
export function formatEquipmentDisplayName(zName, zId) {
|
|
38
|
+
const name = zName?.trim() ?? '';
|
|
39
|
+
const id = zId?.trim() ?? '';
|
|
40
|
+
if (name && !isLocalizationKey(name)) {
|
|
41
|
+
return formatIdentifierTitleCase(name);
|
|
42
|
+
}
|
|
43
|
+
if (id && !isUuidLike(id)) {
|
|
44
|
+
return formatIdentifierTitleCase(id);
|
|
45
|
+
}
|
|
46
|
+
if (name)
|
|
47
|
+
return formatIdentifierTitleCase(name);
|
|
48
|
+
if (id)
|
|
49
|
+
return id;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
export function formatExerciseName(name) {
|
|
53
|
+
const words = name.split('_');
|
|
54
|
+
const lastWord = words.at(-1)?.toLowerCase();
|
|
55
|
+
const shouldWrapLast = lastWord ? PARENTHETICAL_SUFFIXES.has(lastWord) : false;
|
|
56
|
+
const formattedWords = words.map((word, index) => {
|
|
57
|
+
const formatted = formatIdentifierTitleCase(word).replaceAll(' ', '-');
|
|
58
|
+
if (shouldWrapLast && index === words.length - 1)
|
|
59
|
+
return `(${formatted})`;
|
|
60
|
+
return formatted;
|
|
61
|
+
});
|
|
62
|
+
return formattedWords.join(' ');
|
|
63
|
+
}
|
|
64
|
+
export function formatExerciseDisplayName(name, isUserCreated) {
|
|
65
|
+
if (!name)
|
|
66
|
+
return '(unnamed)';
|
|
67
|
+
if (isUserCreated)
|
|
68
|
+
return name;
|
|
69
|
+
return formatExerciseName(name);
|
|
70
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type TableFormatterContext = {
|
|
2
|
+
style: (...args: string[]) => string;
|
|
3
|
+
};
|
|
4
|
+
type TableFormatter = (this: TableFormatterContext, cellValue: unknown) => string;
|
|
5
|
+
export declare const createValueFormatter: (dateFormat: string) => TableFormatter;
|
|
6
|
+
export declare function renderTable(rows: Array<Record<string, unknown>>): string;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { format, isValid, parseISO } from 'date-fns';
|
|
2
|
+
import Table from 'tty-table';
|
|
3
|
+
function formatDateString(value, dateFormat) {
|
|
4
|
+
const parsed = parseISO(value);
|
|
5
|
+
if (!isValid(parsed))
|
|
6
|
+
return null;
|
|
7
|
+
return format(parsed, dateFormat);
|
|
8
|
+
}
|
|
9
|
+
function valueFormatter(cellValue, dateFormat) {
|
|
10
|
+
if (cellValue === null || cellValue === undefined)
|
|
11
|
+
return '';
|
|
12
|
+
if (typeof cellValue === 'string') {
|
|
13
|
+
const formattedDate = formatDateString(cellValue, dateFormat);
|
|
14
|
+
if (formattedDate)
|
|
15
|
+
return this.style(String(formattedDate), 'magenta');
|
|
16
|
+
return cellValue;
|
|
17
|
+
}
|
|
18
|
+
if (typeof cellValue === 'number')
|
|
19
|
+
return this.style(String(cellValue), 'blue');
|
|
20
|
+
if (typeof cellValue === 'boolean') {
|
|
21
|
+
return cellValue ? this.style('✔︎', 'green', 'bold') : this.style('×', 'red', 'bold');
|
|
22
|
+
}
|
|
23
|
+
return this.style(JSON.stringify(cellValue), 'yellow');
|
|
24
|
+
}
|
|
25
|
+
export const createValueFormatter = (dateFormat) => function (cellValue) {
|
|
26
|
+
return valueFormatter.call(this, cellValue, dateFormat);
|
|
27
|
+
};
|
|
28
|
+
function toText(value) {
|
|
29
|
+
if (value === null || value === undefined)
|
|
30
|
+
return '';
|
|
31
|
+
if (typeof value === 'boolean')
|
|
32
|
+
return value ? 'true' : 'false';
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
35
|
+
function visibleWidth(value) {
|
|
36
|
+
const text = toText(value);
|
|
37
|
+
return Math.max(...text
|
|
38
|
+
.split('\n')
|
|
39
|
+
.map((line) => line.length));
|
|
40
|
+
}
|
|
41
|
+
export function renderTable(rows) {
|
|
42
|
+
if (rows.length === 0)
|
|
43
|
+
return '(no rows)';
|
|
44
|
+
const keys = Object.keys(rows[0]);
|
|
45
|
+
const orderedKeys = keys.includes('id')
|
|
46
|
+
? ['id', ...keys.filter((key) => key !== 'id')]
|
|
47
|
+
: keys;
|
|
48
|
+
const widths = new Map();
|
|
49
|
+
const formatter = createValueFormatter('yyyy-MM-dd HH:mm');
|
|
50
|
+
const normalizedRows = rows.map((row) => {
|
|
51
|
+
const output = {};
|
|
52
|
+
for (const key of orderedKeys) {
|
|
53
|
+
const raw = row[key];
|
|
54
|
+
output[key] = raw;
|
|
55
|
+
widths.set(key, Math.max(widths.get(key) ?? key.length, visibleWidth(raw)));
|
|
56
|
+
}
|
|
57
|
+
return output;
|
|
58
|
+
});
|
|
59
|
+
const columns = orderedKeys.map((key) => ({
|
|
60
|
+
alias: key,
|
|
61
|
+
align: 'left',
|
|
62
|
+
formatter,
|
|
63
|
+
headerAlign: 'left',
|
|
64
|
+
value: key,
|
|
65
|
+
width: (widths.get(key) ?? key.length) + 2,
|
|
66
|
+
}));
|
|
67
|
+
// eslint-disable-next-line new-cap
|
|
68
|
+
return Table(columns, normalizedRows, {
|
|
69
|
+
borderStyle: 'solid',
|
|
70
|
+
compact: true,
|
|
71
|
+
width: 'auto',
|
|
72
|
+
}).render();
|
|
73
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Kysely } from 'kysely';
|
|
2
|
+
import { DatabaseSchema } from '../db.js';
|
|
3
|
+
import { ExerciseDetail, ExerciseHistoryRow, ExerciseHistoryWithSetsRow, ExerciseSummary, WorkoutDetail, WorkoutExerciseDetail } from '../types.js';
|
|
4
|
+
export type ExerciseListFilters = {
|
|
5
|
+
equipment?: string;
|
|
6
|
+
muscle?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
sort?: 'lastPerformed' | 'name' | 'timesPerformed';
|
|
9
|
+
};
|
|
10
|
+
export type ExerciseHistoryFilters = {
|
|
11
|
+
from?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
maxReps?: number;
|
|
14
|
+
maxWeight?: number;
|
|
15
|
+
minReps?: number;
|
|
16
|
+
minWeight?: number;
|
|
17
|
+
program?: string;
|
|
18
|
+
routine?: string;
|
|
19
|
+
to?: string;
|
|
20
|
+
};
|
|
21
|
+
export type ExerciseLastPerformedSnapshot = {
|
|
22
|
+
exercise: WorkoutExerciseDetail;
|
|
23
|
+
workout: WorkoutDetail;
|
|
24
|
+
};
|
|
25
|
+
export declare function resolveExerciseSelector(db: Kysely<DatabaseSchema>, selector: string): Promise<number>;
|
|
26
|
+
export declare function listExercises(db: Kysely<DatabaseSchema>, filters: ExerciseListFilters): Promise<ExerciseSummary[]>;
|
|
27
|
+
export declare function getExerciseHistoryRows(db: Kysely<DatabaseSchema>, exerciseId: number, filters: ExerciseHistoryFilters): Promise<ExerciseHistoryRow[]>;
|
|
28
|
+
export declare function getExerciseHistoryWithSetsRows(db: Kysely<DatabaseSchema>, exerciseId: number, filters: ExerciseHistoryFilters): Promise<ExerciseHistoryWithSetsRow[]>;
|
|
29
|
+
export declare function getExerciseDetail(db: Kysely<DatabaseSchema>, exerciseId: number, historyLimit?: number): Promise<ExerciseDetail>;
|
|
30
|
+
export declare function getLastPerformedExerciseSnapshot(db: Kysely<DatabaseSchema>, exerciseId: number): Promise<ExerciseLastPerformedSnapshot | null>;
|