also looking at this
Migrate :text/x-text bindings to {{ }} interpolation (stx 0.2.72)
#990
50 files
+611
-371
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -33,7 +33,9 @@ export default new Action({ | ||
| 33 | 33 | })) |
| 34 | 34 | .sort((a: any, b: any) => Date.parse(b.createdAt ?? '') - Date.parse(a.createdAt ?? '')) |
| 35 | 35 | |
| 36 | const paged = paginate(challenges, readPageParams(request)) | |
| 36 | // Generous default — the page filters the full set into client-side | |
| 37 | // tabs (active/sent/received/completed) and has no load-more (#978 review). | |
| 38 | const paged = paginate(challenges, readPageParams(request, { defaultLimit: 200, maxLimit: 200 })) | |
| 37 | 39 | return response.json({ success: true, challenges: paged.items, meta: paged.meta }) |
| 38 | 40 | } |
| 39 | 41 | catch (error) { |
| @@ -73,7 +73,9 @@ export default new Action({ | ||
| 73 | 73 | }) |
| 74 | 74 | .sort((a: any, b: any) => b.memberCount - a.memberCount || a.id - b.id) |
| 75 | 75 | |
| 76 | const paged = paginate(result, readPageParams(request)) | |
| 76 | // Generous default — the clubs page has no load-more and expects the | |
| 77 | // full set; ?limit/?offset still paginate on demand (#978 review). | |
| 78 | const paged = paginate(result, readPageParams(request, { defaultLimit: 200, maxLimit: 200 })) | |
| 77 | 79 | return response.json({ success: true, clubs: paged.items, meta: paged.meta }) |
| 78 | 80 | } |
| 79 | 81 | catch (error) { |
| @@ -5,8 +5,6 @@ | ||
| 5 | 5 | // returns the most active athletes as a discover list. Public read; response |
| 6 | 6 | // includes enough stats to render discover cards without extra calls. |
| 7 | 7 | |
| 8 | const SEARCH_LIMIT = 20 | |
| 9 | ||
| 10 | 8 | export default new Action({ |
| 11 | 9 | name: 'User Search', |
| 12 | 10 | description: 'Search athletes by name (discover list when no query)', |
| @@ -17,9 +15,12 @@ export default new Action({ | ||
| 17 | 15 | const q = typeof qRaw === 'string' ? qRaw.trim() : '' |
| 18 | 16 | |
| 19 | 17 | try { |
| 18 | // Fetch the full match set, then paginate — so meta.total/hasMore reflect | |
| 19 | // the real count (a DB-side .limit() before paginate() would cap total at | |
| 20 | // the page size and make hasMore always false, #978 review). | |
| 20 | 21 | const users = q.length >= 2 |
| 21 | ? (await User.where('name', 'like', `%${q}%`).limit(SEARCH_LIMIT).get()) ?? [] | |
| 22 | : (await User.query().limit(SEARCH_LIMIT).get()) ?? [] | |
| 22 | ? (await User.where('name', 'like', `%${q}%`).get()) ?? [] | |
| 23 | : (await User.query().get()) ?? [] | |
| 23 | 24 | |
| 24 | 25 | const ids = users.map((u: any) => u.id) |
| 25 | 26 | const activities = ids.length ? (await Activity.whereIn('user_id', ids).get()) ?? [] : [] |
| @@ -48,7 +48,9 @@ export default new Action({ | ||
| 48 | 48 | } |
| 49 | 49 | }) |
| 50 | 50 | |
| 51 | const paged = paginate(savedTrails, readPageParams(request)) | |
| 51 | // Generous default — the profile Saved tab renders the full set, no | |
| 52 | // load-more (#978 review); ?limit/?offset still paginate on demand. | |
| 53 | const paged = paginate(savedTrails, readPageParams(request, { defaultLimit: 200, maxLimit: 200 })) | |
| 52 | 54 | return response.json({ success: true, savedTrails: paged.items, meta: paged.meta }) |
| 53 | 55 | } |
| 54 | 56 | catch (error) { |
| @@ -36,7 +36,9 @@ export default new Action({ | ||
| 36 | 36 | createdAt: r.created_at, |
| 37 | 37 | })) |
| 38 | 38 | |
| 39 | const paged = paginate(reviews, readPageParams(request)) | |
| 39 | // Generous default — the reviews tab renders the full set, no load-more | |
| 40 | // (#978 review); ?limit/?offset still paginate on demand. | |
| 41 | const paged = paginate(reviews, readPageParams(request, { defaultLimit: 200, maxLimit: 200 })) | |
| 40 | 42 | return response.json({ success: true, reviews: paged.items, meta: paged.meta }) |
| 41 | 43 | } |
| 42 | 44 | catch (error) { |
| @@ -23,7 +23,7 @@ export default defineModel({ | ||
| 23 | 23 | order: 1, |
| 24 | 24 | fillable: true, |
| 25 | 25 | validation: { |
| 26 | rule: schema.number().min(0), | |
| 26 | rule: schema.float().min(0), | |
| 27 | 27 | }, |
| 28 | 28 | factory: (faker) => faker.number.float({ min: 10, max: 2000, fractionDigits: 1 }), |
| 29 | 29 | }, |
| @@ -45,7 +45,7 @@ export default defineModel({ | ||
| 45 | 45 | order: 3, |
| 46 | 46 | fillable: true, |
| 47 | 47 | validation: { |
| 48 | rule: schema.number().min(0), | |
| 48 | rule: schema.float().min(0), | |
| 49 | 49 | }, |
| 50 | 50 | factory: (faker) => faker.number.int({ min: 5000, max: 500000 }), |
| 51 | 51 | }, |
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | CREATE TABLE IF NOT EXISTS "user_stats" ( |
| 2 | 2 | "id" INTEGER PRIMARY KEY AUTOINCREMENT, |
| 3 | "total_distance" INTEGER, | |
| 3 | "total_distance" REAL, | |
| 4 | 4 | "total_time" TEXT, |
| 5 | "total_elevation" INTEGER, | |
| 5 | "total_elevation" REAL, | |
| 6 | 6 | "trails_completed" INTEGER, |
| 7 | 7 | "current_streak" INTEGER, |
| 8 | 8 | "longest_streak" INTEGER, |
| @@ -35,6 +35,7 @@ const REAL_COLS: Array<[string, string]> = [ | ||
| 35 | 35 | ['territory_stats', 'total_area_owned'], ['territory_stats', 'largest_territory_area'], |
| 36 | 36 | ['challenges', 'area_at_stake'], |
| 37 | 37 | ['territory_histories', 'area_at_event'], |
| 38 | ['user_stats', 'total_distance'], ['user_stats', 'total_elevation'], | |
| 38 | 39 | ] |
| 39 | 40 | for (const [t, c] of REAL_COLS) |
| 40 | 41 | check(`${t}.${c} is REAL`, colType(t, c) === 'REAL', colType(t, c)) |
| @@ -47,6 +48,7 @@ const INT_COLS: Array<[string, string]> = [ | ||
| 47 | 48 | ['trails', 'review_count'], |
| 48 | 49 | ['territory_stats', 'total_territories_owned'], ['territory_stats', 'territories_conquered'], ['territory_stats', 'weekly_rank'], |
| 49 | 50 | ['challenges', 'challenger_id'], ['challenges', 'winner_id'], |
| 51 | ['user_stats', 'trails_completed'], ['user_stats', 'current_streak'], ['user_stats', 'total_activities'], | |
| 50 | 52 | ['reviews_alias_skip', 'noop'], |
| 51 | 53 | ] |
| 52 | 54 | for (const [t, c] of INT_COLS) { |
| @@ -50,6 +50,7 @@ globalThis.Error = RealError | ||
| 50 | 50 | const ActivityIndex = (await import('../app/Actions/Activity/ActivityIndexAction')).default |
| 51 | 51 | const TrailIndex = (await import('../app/Actions/Trail/TrailIndexAction')).default |
| 52 | 52 | const ClubIndex = (await import('../app/Actions/Club/ClubIndexAction')).default |
| 53 | const UserSearch = (await import('../app/Actions/Social/UserSearchAction')).default | |
| 53 | 54 | |
| 54 | 55 | async function call(action: any, body: Record<string, unknown>): Promise<{ status: number, json: any }> { |
| 55 | 56 | const request = { get: (k: string) => (body as any)[k] } |
| @@ -89,5 +90,16 @@ const cAll = await call(ClubIndex, {}) | ||
| 89 | 90 | check('ClubIndex has pagination meta', typeof cAll.json?.meta?.offset === 'number' |
| 90 | 91 | && typeof cAll.json?.meta?.hasMore === 'boolean') |
| 91 | 92 | |
| 93 | // UserSearch: total must be the REAL match count, not capped at the page size | |
| 94 | // (the DB-side .limit() before paginate() was removed, #978 review). | |
| 95 | const sAll = await call(UserSearch, {}) | |
| 96 | const userTotal = sAll.json?.meta?.total | |
| 97 | check('UserSearch total = real match count', typeof userTotal === 'number' && userTotal >= 3 | |
| 98 | && sAll.json?.athletes?.length === userTotal, JSON.stringify(sAll.json?.meta)) | |
| 99 | const sPage = await call(UserSearch, { limit: 2 }) | |
| 100 | check('UserSearch limit=2: 2 items, total unchanged, hasMore true (not capped)', | |
| 101 | sPage.json?.athletes?.length === 2 && sPage.json?.meta?.total === userTotal | |
| 102 | && sPage.json?.meta?.hasMore === (userTotal > 2), JSON.stringify(sPage.json?.meta)) | |
| 103 | ||
| 92 | 104 | console.log(failures === 0 ? '\n✅ all pagination checks passed' : `\n❌ ${failures} check(s) failed`) |
| 93 | 105 | process.exit(failures === 0 ? 0 : 1) |