chore(audit): clean exports/types, fix mutations, rename Spotify search, Vercel method, and remove unused#12
Conversation
- Update package.json with new dependencies/configuration - Enhance error exports structure - Refactor resource factory implementation - Extend core types definitions - Update TypeScript configuration
- Remove redundant examples/API_CORE_EXAMPLES.md with outdated architecture - Rewrite README.md to be cleaner and more focused - Remove emojis, keep npm badges - Focus on 6 supported APIs with enhanced features - Add comprehensive examples for all APIs - Document advanced features (caching, rate limiting, error handling) - Add SECURITY.md with comprehensive security guidelines - API token security best practices - Environment variable setup - Production deployment security - Common security pitfalls - Clean up package.json - Remove unused 'glob' dependency - Optimize build scripts - Update keywords to reflect actual features - Add SECURITY.md to published files - All changes maintain version 4.0.0 consistency
…export, fix mutation call signatures, rename Spotify search method, add Vercel createDeployment, clean unused files, and switch module factory to simple client - Remove wildcard re-export of api-factory-enhanced to avoid type collisions - Make TApiClient and TApiConfig explicit exports; stop re-exporting conflicting TResult - Fix POST/PUT/PATCH call signature to (data, options) and move path params to options - Rename Spotify search convenience to searchAll to avoid clash with resource - Add deployments.createDeployment and correct Vercel headers typing - Remove unused core/http, unified-config, normalize, and method-factory references - Use base api client in module-factory; export necessary types - Make resource-factory safer re: method narrowing - Typecheck passes (tsc --noEmit)
WalkthroughAdds a full GitLab API module with extensive typed resources and helpers, updates exports and examples, and bumps package to 4.0.0. Core public types and error/http type surfaces are removed or narrowed, with minor core factory tweaks. Significant documentation overhaul adds new architecture/style guides and security policies; some legacy docs/examples are deleted. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Dev as Developer
participant GL as GitLab(config)
participant AB as createApiBuilder
participant RES as Resources
participant API as GitLab REST API
Dev->>GL: GitLab({ token })
activate GL
GL->>AB: createApiBuilder({ baseUrl, headers: Bearer token })
AB-->>GL: api client
GL->>RES: define resources (users, projects, groups, ...)
RES-->>GL: resource methods
GL-->>Dev: module with helpers
note over Dev,GL: Example helper: getProjectFromUrl(url)
Dev->>GL: getProjectFromUrl("https://gitlab.com/group/proj")
activate GL
GL->>GL: parseGitLabUrl(url) -> "group/proj"
GL->>API: GET /projects/{encoded path}
API-->>GL: 200 Project JSON
GL-->>Dev: Project
deactivate GL
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Docstrings generation was requested by @remcostoeten. * #12 (comment) The following files were modified: * `fync/examples/unified-usage.ts` * `fync/src/core/api-factory-enhanced.ts` * `fync/src/core/api-factory.ts` * `fync/src/core/module-factory.ts` * `fync/src/core/resource-factory.ts` * `fync/src/github/index.ts` * `fync/src/gitlab/index.ts` * `fync/src/google-calendar/index.ts` * `fync/src/google-drive/index.ts` * `fync/src/npm/index.ts` * `fync/src/spotify/index.ts` * `fync/src/vercel/index.ts`
|
Note Generated docstrings for this pull request at #13 |
There was a problem hiding this comment.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (12)
IMPLEMENTED_METHODS.md (2)
127-150: Rename Spotify convenience method to searchAll to avoid collision with the search resourcePR objective states the convenience method was renamed from search to searchAll. The doc still shows search(query, ...), which collides in naming with the search resource.
Apply this diff:
- - `search(query, types, options?)` - Multi-type search + - `searchAll(query, types, options?)` - Multi-type search across multiple typesI can sweep examples/README snippets in the repo to align any references if you want.
153-159: Add Vercel deployments.createDeployment and update method countsThe PR objective adds deployments.createDeployment (POST /v13/deployments). It’s missing from the deployments list. Also, adding it bumps the Vercel total by +1 and the global total by +1.
Apply these diffs:
-**Vercel** - 36 total methods +**Vercel** - 37 total methods- - **deployments**: `listDeployments`, `getDeployment`, `deleteDeployment`, `getDeploymentEvents`, `getDeploymentFiles`, `cancelDeployment` + - **deployments**: `listDeployments`, `getDeployment`, `createDeployment`, `deleteDeployment`, `getDeploymentEvents`, `getDeploymentFiles`, `cancelDeployment`-### Total: 428 methods implemented across all 7 modules +### Total: 429 methods implemented across all 7 modulesfync/src/core/module-factory.ts (1)
60-62: Token propagation is a no-op when defaultConfig.auth is missingAs written, if userConfig.token is provided but finalConfig.auth isn’t preset to bearer, the token is silently ignored. This is brittle across modules.
Apply this diff to make token handling robust:
- if (userConfig.token && finalConfig.auth?.type === "bearer") { - finalConfig.auth.token = userConfig.token; - } + if (userConfig.token) { + if (!finalConfig.auth) { + finalConfig.auth = { type: "bearer", token: userConfig.token } as any; + } else if (finalConfig.auth.type === "bearer") { + finalConfig.auth.token = userConfig.token; + } + }If you intentionally rely on per-module defaults to set auth.type, at least add a comment to document the contract. Otherwise, this change removes a subtle footgun.
fync/src/google-drive/index.ts (2)
232-238: Escape user input in Drive query strings to avoid malformed queries.Interpolating raw values into the q parameter can break the Drive query when the string contains quotes or operators.
Add a small helper and use it:
@@ - drive.searchFiles = async function (query: string) { + function escapeDriveQueryValue(v: string) { + return String(v).replaceAll("'", "\\'"); + } + drive.searchFiles = async function (query: string) { const response = await base.files.listFiles({ - q: `name contains '${query}'`, + q: `name contains '${escapeDriveQueryValue(query)}'`, fields: "files(id, name, mimeType, size, createdTime, modifiedTime)", }); @@ - drive.getFilesByName = async function (name: string) { + drive.getFilesByName = async function (name: string) { const response = await base.files.listFiles({ - q: `name = '${name}'`, + q: `name = '${escapeDriveQueryValue(name)}'`, fields: "files(id, name, mimeType, size, createdTime, modifiedTime)", });If the core builder supports query parameter encoding instead, prefer passing raw values via a structured options object to avoid manual string assembly.
Also applies to: 240-246
177-180: Fix Drive mutation wrappers to use (data, options) signatureThe Drive wrappers for updateFile, copyFile, moveFile, and restoreFile still embed the
fileId(and other path params) into the body object. This breaks path‐parameter substitution and may leak those fields into the request body. Please update each wrapper to pass the request body as the first argument and path placeholders in a second options object.Affected locations in fync/src/google-drive/index.ts:
- Lines 178–180 (drive.updateFile)
- Lines 186–189 (drive.copyFile)
- Lines 191–197 (drive.moveFile)
- Lines 312–314 (drive.restoreFile)
Apply these diffs:
drive.updateFile = function (fileId: string, metadata?: any, content?: any) { - const body = content ? { ...metadata, media: content } : metadata; - return base.files.updateFile({ fileId, ...body }); + const body = content ? { ...metadata, media: content } : metadata; + return base.files.updateFile(body, { fileId }); }; drive.copyFile = function (fileId: string, name?: string) { - const body = name ? { name } : {}; - return base.files.copyFile({ fileId, ...body }); + const body = name ? { name } : {}; + return base.files.copyFile(body, { fileId }); }; drive.moveFile = function (fileId: string, parentId: string) { - return base.files.updateFile({ - fileId, - addParents: parentId, - removeParents: "root", - }); + return base.files.updateFile( + { addParents: parentId, removeParents: "root" }, + { fileId }, + ); }; drive.restoreFile = function (fileId: string) { - return base.files.updateFile({ fileId, trashed: false }); + return base.files.updateFile({ trashed: false }, { fileId }); };fync/src/google-calendar/index.ts (1)
270-288: Mutations still use legacy single-arg signature; switch to (data, options) to avoid path/body collisions.Per PR objective (“Fix mutation signatures”), POST/PUT/DELETE should pass body as first arg and path placeholders via options second arg. Current implementations merge placeholders into the body, risking runtime bugs and conflicting keys.
Apply this diff:
- calendar.createEvent = function (calendarId: string, event: any) { - return base.events.insertEvent({ calendarId, ...event }); - }; + calendar.createEvent = function (calendarId: string, event: any) { + return base.events.insertEvent(event, { calendarId }); + }; - calendar.updateEvent = function ( - calendarId: string, - eventId: string, - event: any, - ) { - return base.events.updateEvent({ calendarId, eventId, ...event }); - }; + calendar.updateEvent = function ( + calendarId: string, + eventId: string, + event: any, + ) { + return base.events.updateEvent(event, { calendarId, eventId }); + }; - calendar.deleteEvent = function (calendarId: string, eventId: string) { - return base.events.deleteEvent({ calendarId, eventId }); - }; + calendar.deleteEvent = function (calendarId: string, eventId: string) { + // No body for DELETE; pass placeholders via options + return base.events.deleteEvent(undefined, { calendarId, eventId }); + }; - calendar.quickAddEvent = function (calendarId: string, text: string) { - return base.events.quickAddEvent({ calendarId, text }); - }; + calendar.quickAddEvent = function (calendarId: string, text: string) { + // quickAdd expects text as a query param; empty body + params in options + return base.events.quickAddEvent({}, { calendarId, text }); + };fync/src/vercel/index.ts (1)
20-31: Missing deployments.createDeployment endpoint definition causes runtime error.
vercel.redeployProjectcallsbase.deployments.createDeployment(...), but the resource lacks this method. This will throw at runtime.Add the method:
const deploymentResource = defineResource({ name: "deployments", basePath: "/v13/deployments", methods: { listDeployments: { path: "" }, getDeployment: { path: "/{deploymentId}" }, + createDeployment: { path: "", method: "POST" }, deleteDeployment: { path: "/{deploymentId}", method: "DELETE" }, getDeploymentEvents: { path: "/{deploymentId}/events" }, getDeploymentFiles: { path: "/{deploymentId}/files" }, cancelDeployment: { path: "/{deploymentId}/cancel", method: "PATCH" }, }, });fync/src/spotify/index.ts (2)
159-165: Rename convenience search to searchAll to avoid resource collision (per PR objective).The PR objective says “Rename Spotify convenience method: search → searchAll.” Current code reintroduces
searchby casting to any, shadowing thesearchresource and defeating the goal.Apply this cohesive change:
- type TSpotifyModule = TModule<typeof resources> & { + type TSpotifyModule = TModule<typeof resources> & { getTrack: (trackId: string) => Promise<any>; getArtist: (artistId: string) => Promise<any>; getAlbum: (albumId: string) => Promise<any>; getPlaylist: (playlistId: string) => Promise<any>; - search: (query: string, types: string[], options?: any) => Promise<any>; + searchAll: (query: string, types: Array<"track" | "artist" | "album" | "playlist">, options?: any) => Promise<any>; getMyTopTracks: (options?: any) => Promise<any>; getMyTopArtists: (options?: any) => Promise<any>; getRecentlyPlayed: (options?: any) => Promise<any>; getCurrentlyPlaying: () => Promise<any>; getRecommendations: (options: any) => Promise<any>; createPlaylist: (userId: string, name: string, options?: any) => Promise<any>; addTracksToPlaylist: ( playlistId: string, trackUris: string[], ) => Promise<any>; playTrack: (trackUri: string, deviceId?: string) => Promise<any>; pausePlayback: () => Promise<any>; skipToNext: () => Promise<any>; skipToPrevious: () => Promise<any>; getUserPlaylists: (userId?: string) => Promise<any>; getAudioFeatures: (trackId: string) => Promise<any>; searchTracks: (query: string, options?: any) => Promise<any>; searchArtists: (query: string, options?: any) => Promise<any>; searchAlbums: (query: string, options?: any) => Promise<any>; searchPlaylists: (query: string, options?: any) => Promise<any>; };- (spotify as any).search = function (query: string, types: string[], options?: any) { + spotify.searchAll = function (query: string, types: Array<"track" | "artist" | "album" | "playlist">, options?: any) { return base.search.search({ q: query, type: types.join(","), limit: options?.limit || 20, offset: options?.offset || 0, ...options, }); };- spotify.searchTracks = function (query: string, options?: any) { - return spotify.search(query, ["track"], options); - }; + spotify.searchTracks = function (query: string, options?: any) { + return spotify.searchAll(query, ["track"], options); + }; - spotify.searchArtists = function (query: string, options?: any) { - return spotify.search(query, ["artist"], options); - }; + spotify.searchArtists = function (query: string, options?: any) { + return spotify.searchAll(query, ["artist"], options); + }; - spotify.searchAlbums = function (query: string, options?: any) { - return spotify.search(query, ["album"], options); - }; + spotify.searchAlbums = function (query: string, options?: any) { + return spotify.searchAll(query, ["album"], options); + }; - spotify.searchPlaylists = function (query: string, options?: any) { - return spotify.search(query, ["playlist"], options); - }; + spotify.searchPlaylists = function (query: string, options?: any) { + return spotify.searchAll(query, ["playlist"], options); + };This removes the need for the
(spotify as any)escape hatch and prevents overriding thesearchresource.Also applies to: 207-215, 304-318
11-12: Update Spotify audio features/analysis endpoint pathsThe Spotify Web API defines these endpoints at the root level, not under
/tracks/{id}. Using the current paths will result in 404s. Please update as follows:
- fync/src/spotify/index.ts (around lines 11–12):
- getTrackAudioFeatures: { path: "/{id}/audio-features" },
- getTrackAudioAnalysis: { path: "/{id}/audio-analysis" },
- getTrackAudioFeatures: { path: "/audio-features/{id}" }, // GET /v1/audio-features/{id} (developer.spotify.com)
- getTrackAudioAnalysis: { path: "/audio-analysis/{id}" }, // GET /v1/audio-analysis/{id} (developer.spotify.com)
- Also apply the same changes around lines 300–302 where these endpoints are referenced. </blockquote></details> <details> <summary>fync/examples/unified-usage.ts (1)</summary><blockquote> `193-197`: **Rename all remaining `spotify.search` calls to `spotify.searchAll`** The grep output shows that several convenience calls weren’t updated as part of the PR: - **fync/examples/unified-usage.ts** (line 193) - **fync/src/spotify/index.ts** (lines 305, 309, 313, 317) Please update each invocation of the old method to use the new name. For example: ```diff - const multiSearch = await spotify.search("queen", ["artist", "track", "album"], { + const multiSearch = await spotify.searchAll("queen", ["artist", "track", "album"], { limit: 10, offset: 0, market: "US", });And in
fync/src/spotify/index.ts:- return spotify.search(query, ["track"], options); + return spotify.searchAll(query, ["track"], options);(Apply similarly at lines 309, 313, and 317.)
fync/src/core/api-factory.ts (2)
35-53: Avoid emitting auth headers with missing credentials; optionally fail fast.Today,
bearer/oauth2will sendAuthorization: Bearer undefinediftokenis absent; same forbasic/apikeywith missing credentials. That’s brittle and can leak intent.Safer default:
case "bearer": - return { Authorization: `Bearer ${auth.token}` }; + return auth.token ? { Authorization: `Bearer ${auth.token}` } : {}; case "basic": { const { username = "", password = "" } = auth.credentials || {}; - const encoded = Buffer.from(`${username}:${password}`).toString("base64"); - return { Authorization: `Basic ${encoded}` }; + if (!username && !password) return {}; + const encoded = Buffer.from(`${username}:${password}`).toString("base64"); + return { Authorization: `Basic ${encoded}` }; } case "apikey": - return { "X-API-Key": auth.key || "" }; + return auth.key ? { "X-API-Key": auth.key } : {}; case "oauth2": - return { Authorization: `Bearer ${auth.token}` }; + return auth.token ? { Authorization: `Bearer ${auth.token}` } : {};Optionally, throw on missing required credentials for stricter behavior.
79-114: Handle non‑JSON/empty responses and avoid JSON.stringify for non‑JSON bodies.Two issues:
- 204/205/304 (or empty body) will throw on
response.json().- File uploads or form submissions break when we unconditionally JSON.stringify.
Robust request body/response handling:
- if (options?.body && method !== "GET") { - fetchOptions.body = JSON.stringify(options.body); - } + // Body handling: only JSON-stringify plain objects; let fetch set headers for FormData/Blob/ArrayBuffer + if (method !== "GET" && options?.body !== undefined) { + const b = options.body as unknown; + const isFormLike = + typeof FormData !== "undefined" && b instanceof FormData || + (typeof Blob !== "undefined" && b instanceof Blob) || + (typeof ArrayBuffer !== "undefined" && b instanceof ArrayBuffer) || + (typeof URLSearchParams !== "undefined" && b instanceof URLSearchParams); + if (isFormLike) { + // Let fetch set Content-Type including boundaries + // Caller can still override via options.headers + fetchOptions.body = b as any; + delete (headers as any)["Content-Type"]; + } else if (typeof b === "string" || b instanceof Uint8Array) { + fetchOptions.body = b as any; + } else { + fetchOptions.body = JSON.stringify(b); + } + } ... - const data = await response.json(); - return data as T; + // Response handling: parse JSON only when appropriate + const ct = response.headers.get("content-type") || ""; + if (response.status === 204 || response.status === 205 || response.status === 304) { + return undefined as T; + } + if (!ct || !ct.includes("application/json")) { + const text = await response.text(); + // Best effort: return text when non-JSON + return text as unknown as T; + } + const data = await response.json(); + return data as T;
🧹 Nitpick comments (59)
fync/tsconfig.json (1)
10-12: Deduplicate skipLibCheck and normalize indentation in compilerOptionsYou’ve got two skipLibCheck entries (Lines 8 and 12). TS reads the last occurrence, but duplication is confusing. Also, the changed lines are tab-indented whereas the rest of the file uses spaces.
Apply this diff:
- "declaration": true, - "declarationMap": true, - "skipLibCheck": true, + "declaration": true, + "declarationMap": true,Optional: if you’re standardizing paths for new modules in this PR (GitLab, Google Drive/Calendar, Vercel), consider adding/removing path aliases consistently in this block in a follow-up.
IMPLEMENTED_METHODS.md (1)
3-3: Refresh “Updated” date for accuracy (optional)The header says “Updated Jan 27 2025” while this PR is dated Aug 24, 2025. Consider updating for clarity or removing the date if it’s maintained elsewhere.
-## Summary (Updated Jan 27 2025) +## Summary (Updated Aug 24, 2025)fync/src/gitlab/types/gitlab-common.ts (2)
13-19: Prefer an exported const for member access levels to enable reuse at runtimeDefining TGitLabMemberAccess as a type describing an object is less practical than exporting a const value and deriving the type. This allows reuse for validation/mapping at runtime and clean type inference.
-export type TGitLabMemberAccess = { - guest: 10; - reporter: 20; - developer: 30; - maintainer: 40; - owner: 50; -}; +export const GITLAB_MEMBER_ACCESS = { + guest: 10, + reporter: 20, + developer: 30, + maintainer: 40, + owner: 50, +} as const; +export type TGitLabMemberAccess = typeof GITLAB_MEMBER_ACCESS; +export type TGitLabAccessLevel = typeof GITLAB_MEMBER_ACCESS[keyof typeof GITLAB_MEMBER_ACCESS];
1-7: Verify GitLab config alignment with core TApiConfigTGitLabConfig exposes token, baseUrl, cache, cacheTTL, userAgent. Ensure these map cleanly to core TApiConfig used by createFyncApi/createApiBuilder (headers/auth). In particular:
- userAgent should be injected into headers as "User-Agent".
- cache/cacheTTL should be either honored by the core client or removed to avoid a misleading surface.
Happy to wire a small adapter in the GitLab module that maps TGitLabConfig → TApiConfig to guarantee consistency.
fync/src/core/module-factory.ts (1)
26-37: Guard reserved key collisions and tighten helper typingResources can overwrite the reserved api property; and helpers are typed as Function. Minor hardening improves safety.
-const apiClient = createFyncApi(config.apiConfig); -const module = { api: apiClient } as TModule<TResources>; +const apiClient = createFyncApi(config.apiConfig); +const module = { api: apiClient } as TModule<TResources>; +// Prevent collisions with reserved keys +if (Object.prototype.hasOwnProperty.call(config.resources, "api")) { + throw new Error("Resource name 'api' is reserved"); +}Optionally change the helpers type in TModuleConfig to
Record<string, (...args: any[]) => any>to avoid the very-broad Function type.fync/src/npm/index.ts (1)
73-73: Casting through unknown is acceptable but avoidable.The pattern base as unknown as TNpmModule is a pragmatic escape hatch. Consider tightening the return type of createApiBuilder via generics so the augmentation can be safely expressed with a single assertion or none.
-const npm = base as unknown as TNpmModule; +// If createApiBuilder<TResources, TAugment>() can parameterize augmentation, +// this extra 'unknown' hop becomes unnecessary. +const npm = base as TNpmModule;If changing core typings is out of scope, keep the current line; it’s consistent with the module pattern elsewhere.
fync/src/github/index.ts (2)
259-259: Cast-through-unknown aligns with other modules.Fine to keep for consistency. If you later tighten createApiBuilder’s return type, you can drop the unknown hop.
305-308: Starred count likely undercounts due to pagination.getUserStarredCount uses a single page; GitHub paginates starred repos. If accuracy matters, follow Link headers or iterate pages with per_page=100 until exhausted.
- github.getUserStarredCount = async function (username: string) { - const starred = await base.users.getUserStarred({ username }); - return Array.isArray(starred) ? starred.length : 0; - }; + github.getUserStarredCount = async function (username: string) { + let page = 1, total = 0, batch: any[] = []; + do { + batch = await base.users.getUserStarred({ username, per_page: 100, page }); + total += Array.isArray(batch) ? batch.length : 0; + page += 1; + } while (Array.isArray(batch) && batch.length === 100); + return total; + };fync/src/google-calendar/index.ts (1)
255-264: Avoid swallowing errors when aggregating events across calendars.Silent
catch {}hides useful failures (e.g., permission issues or rate limits), making debugging hard and masking partial results.For example, collect errors and return them alongside results or at least log with context:
- try { + try { const eventsResponse = await base.events.listEvents({ calendarId: cal.id, maxResults, singleEvents: true, orderBy: "startTime", }); results.push({ calendar: cal, events: eventsResponse.items || [] }); - } catch {} + } catch (err) { + // Option A: push an error record + results.push({ calendar: cal, events: [], error: err instanceof Error ? err.message : String(err) }); + // Option B: or minimally log + // console.warn("Failed to list events for calendar", cal.id, err); + }fync/src/core/api-factory-enhanced.ts (1)
105-113: Preset selection works; prefer authenticated preset when a token is present.Current order picks
defaultbeforeauthenticated. For providers like GitHub (nodefault), you already fall through; for others, it’s fine. As a refinement, preferauthenticatedwhen a bearer token exists, thendefault, thenunauthenticated.-const ratePreset = typeof config.rateLimit === "string" - ? (() => { - const preset = RATE_LIMIT_PRESETS[config.rateLimit as keyof typeof RATE_LIMIT_PRESETS] as any; - return preset?.default ?? preset?.authenticated ?? preset?.unauthenticated ?? undefined; - })() - : config.rateLimit; +const ratePreset = typeof config.rateLimit === "string" + ? (() => { + const preset = RATE_LIMIT_PRESETS[config.rateLimit as keyof typeof RATE_LIMIT_PRESETS] as any; + if (config.auth?.type === "bearer" && preset?.authenticated) return preset.authenticated; + return preset?.default ?? preset?.unauthenticated ?? undefined; + })() + : config.rateLimit;README.md (2)
96-109: Double‑check GitLab getUser argument: GitLab’s GET /users/{id} expects a numeric ID.Your example passes a username string, which likely 404s against the canonical endpoint. If your convenience wrapper truly supports username, please call that out. Otherwise, update the snippet to use a numeric ID or provide a username-based helper.
Suggested doc tweak (numeric ID example):
-// Get user info -const user = await gitlab.getUser('remcostoeten') +// Get user info (GitLab expects numeric user ID) +const user = await gitlab.getUser(278964)Would you like me to add a lightweight
getUserByUsername(username: string)convenience that resolves the ID and then callsgetUser(id)?
259-272: Minor phrasing polish (optional).“User and group information — Get profiles, followers, following...” reads a bit GitHub‑ish. If GitLab objects don’t expose “followers/following” consistently, consider softening this bullet to “profile info and relationships.”
fync/src/core/resource-factory.ts (3)
25-42: interpolatePath only replaces the first occurrence per key; support repeated placeholders and safer replacement.Current
String.replacewill replace only once. Rare, but easy to make robust and still readable.Apply:
- if (path.includes(placeholder)) { - path = path.replace(placeholder, encodeURIComponent(String(value))); - } else { + if (path.includes(placeholder)) { + path = path.replaceAll(placeholder, encodeURIComponent(String(value))); + } else {Optionally, consider handling array query values by appending multiple entries instead of
String(value)on arrays.
66-69: Unreachable "GET" fallback in POST/PUT/PATCH branch; simplify and strengthen typing.Inside this branch,
definition.methodis already one of POST/PUT/PATCH. The"GET"fallback is dead code and obscures intent.- const response = await apiClient[ - (definition.method || "GET").toLowerCase() as "post" | "put" | "patch" - ](path, data, { params: queryParams }); + const verb = definition.method!.toLowerCase() as "post" | "put" | "patch"; + const response = await apiClient[verb](path, data, { params: queryParams });
18-22: Consider carrying method-specific data/response typing through TResource (future improvement).Returning
Promise<any>everywhere limits type safety. If/when you’re ready, extendTMethodDefinitionwith generics like<TReq = unknown, TRes = unknown>and thread them throughTResource. Not blocking for this PR.fync/examples/unified-usage.ts (1)
13-15: GitLab getUser likely needs numeric ID (same as README note).If your wrapper supports username, please document it. Otherwise switch to a numeric ID to keep examples copy‑pasteable.
-const user = await gitlab.getUser("remcostoeten"); +const user = await gitlab.getUser(278964);fync/src/core/api-factory.ts (2)
73-77: Header precedence: let per-config headers override auth if desired (optional).Current order places
authHeadersafterconfig.headers, making it hard to overrideAuthorizationper-config. Consider swapping the merge order or documenting the precedence.-const defaultHeaders = { - "Content-Type": "application/json", - ...config.headers, - ...authHeaders, -}; +const defaultHeaders = { + "Content-Type": "application/json", + ...authHeaders, + ...config.headers, // allow user-provided headers to win +};
41-45: Browser compatibility note (optional): Buffer usage ties this to Node.
Bufferis Node‑only. If browser support is intended later, provide a safe branch usingbtoaor a small base64 helper whenBufferis undefined.fync/package.json (4)
3-3: Major bump to 4.0.0—ensure breaking changes are documented.At minimum, call out the Spotify convenience rename (
search➜searchAll) and the tightened core type exports in a CHANGELOG or release notes.I can draft a succinct CHANGELOG entry for 4.0.0 if you want.
63-65: Build pipeline assumption: tsc must output to dist/ for Babel step.Make sure tsconfig.json sets
outDir: "dist"; otherwisebabel dist ...will fail. If not guaranteed, considerrimraf dist-cjs && mkdir -p dist-cjspre-step and a guard in CI.
74-76: Release scripts are convenient but can create two commits per release.
npm versioncreates its own commit/tag; combined with the pre‑commit here, you’ll often get an extra empty commit. That’s fine, but if you want a single canonical version bump commit, drop the manualgit add/commit.
117-118: Undici as a runtime dependency might be unnecessary on Node 18+.You aren’t importing it, and Node 18+ ships fetch. Consider removing to reduce install size unless you plan to use Undici APIs directly.
fync/src/gitlab/types/gitlab-user.ts (2)
89-89: Specialize base entity IDs to numberTighten
idtonumberat the base by using the generic:TBaseEntity<number>. This avoids accidental widening tostring | number.-} & TBaseEntity; +} & TBaseEntity<number>;Repeat the same change at Lines 98, 115, and 141 for all GitLab user-related types.
Also applies to: 98-98, 115-115, 141-141
63-74: Factor status into a reusable type
statusis a coherent sub-structure used across GitLab entities. Extracting it intoTGitLabUserStatusimproves reuse and cohesion.Proposed addition (in the same file or gitlab-common.ts):
export type TGitLabUserStatus = { emoji: string; message: string; message_html: string; availability: "not_set" | "busy"; clear_status_at: string | null; }; export type TGitLabUser = { // ... status?: TGitLabUserStatus | null; } & TBaseEntity<number>;WARP.md (2)
87-87: Fix code fence language specifier
ts path=null start=nullbreaks some Markdown tooling. Use a plain language identifier.-```ts path=null start=null +```ts
11-31: Unify package manager commands to avoid confusionYou recommend
pnpm installbut usenpm runfor scripts. Either show both variants or standardize on one to reduce friction for newcomers.Example reword:
- Install deps:
pnpm install(ornpm install)- Build ESM/CJS:
pnpm build && pnpm build:cjs(ornpm run build && npm run build:cjs)fync/src/gitlab/types/gitlab-project.ts (4)
159-166: Doc comment mismatch: autoclose vs auto devopsThe comment says “Auto devops enabled” but the field is
autoclose_referenced_issues. Adjust the comment to reflect the actual field.- /** Auto devops enabled */ - autoclose_referenced_issues: boolean; + /** Autoclose referenced issues */ + autoclose_referenced_issues: boolean;
287-287: Constrain access level to a known enum, not a bare numberGitLab access levels are a fixed set (e.g., 0, 5, 10, 20, 30, 40, 50). Encoding them as
numberloses safety and IDE help.Add in gitlab-common.ts:
export type TGitLabAccessLevel = 0 | 5 | 10 | 20 | 30 | 40 | 50;Then update here:
- /** Access level */ - access_level: number; + /** Access level */ + access_level: TGitLabAccessLevel;Also applies to: 292-292
271-271: Narrow base entity id to numberSame suggestion as for user types: specialize
TBaseEntity<number>to keepidprecise.-} & TBaseEntity; +} & TBaseEntity<number>;
235-239: Self-referential type can bloat declarations
forked_from_project?: TGitLabProject;introduces deep recursion in.d.ts. Consider a lightweight reference type to avoid large emit graphs.export type TGitLabProjectRef = Pick<TGitLabProject, "id" | "name" | "path_with_namespace" | "web_url">;- /** Forked from project */ - forked_from_project?: TGitLabProject; + /** Forked from project (lightweight ref) */ + forked_from_project?: TGitLabProjectRef;GEMINI.md (2)
31-38: Add language to fenced code block (fixes MD040)Specify a language for the file tree to satisfy markdownlint and improve rendering.
-``` +```text src/ ├── [api-name]/ │ ├── index.ts # Main API implementation │ ├── types.ts # API-specific types (optional) │ └── README.md # Usage documentation (optional) └── core/ # Shared architecture (DO NOT MODIFY)
67-67: Remove trailing punctuation in headings (fixes MD026)Headings should not end with a colon.
-#### Resource Rules: +#### Resource Rules -#### Method Definition Rules: +#### Method Definition Rules -#### Auth Types: +#### Auth TypesAlso applies to: 72-72, 100-100
.cursor/rules/fync-architecture.mdc (1)
221-230: Clarify body/options calling convention with a non-GET example and highlight path placeholders.Good guidance, but add a short note that “options” must include all
{param}placeholders for the path. This prevents common misuse when POST endpoints still have path params.I can add a one-liner below the example if you want.
Also applies to: 246-254
fync/src/gitlab/types/gitlab-snippet.ts (1)
5-13: Prefer TBaseEntity (or remove duplicate id) to avoid redundant declarations.You declare
id: numberand also intersect withTBaseEntity, which also carriesid. Make the intent explicit and avoid drift.Apply:
-export type TGitLabSnippet = { - /** Snippet ID */ - id: number; +export type TGitLabSnippet = { + /** Snippet ID */ + id: number; /** Snippet title */ title: string; /** Snippet file name */ file_name: string; @@ -} & TBaseEntity; +} & TBaseEntity<number>;Optionally drop the explicit
id: number;field and rely solely onTBaseEntity<number>for consistency across types. I can batch-apply that across GitLab types.Also applies to: 37-37
fync/src/gitlab/types/gitlab-group.ts (2)
31-33: Potential redundancy withpublicvsvisibility.A
public: booleanflag can drift fromvisibility === "public". Prefer a single source of truth.Either remove
publicor mark it deprecated in a comment if it exists in legacy payloads.Also applies to: 13-15
4-7: ConsiderTBaseEntity<number>to make numeric IDs explicit.Same pattern as other types: intersecting with
TBaseEntity<number>makes the intent explicit and avoids id type drift.Apply:
-} & TBaseEntity; +} & TBaseEntity<number>;Also applies to: 171-171
fync/src/gitlab/index.ts (3)
41-45: Reminder: repository file endpoints require arefquery parameter.
/projects/{id}/repository/files/{file_path}and the README variant will 400 without aref. Ensure callers pass{ ref: "main" }(or similar). Optionally provide a convenience method that discovers the default branch and supplies it.I can add a
getProjectReadmeAtDefaultBranch(projectId)helper that looks up the default branch then fetches the README.
16-17: Check existence of followers/following endpoints.
/users/{id}/followersand/users/{id}/followingdon’t appear in many GitLab deployments. If not supported, remove to avoid 404s.I can scrub the module after you confirm.
208-226: Type the convenience methods with your public GitLab types.Everything returns
Promise<any>. Given you added rich types underfync/src/gitlab/types, wire them in for better DX.Example:
-type TGitLabModule = TModule<typeof resources> & { - getUser: (id: string | number) => Promise<any>; - getProject: (id: string | number) => Promise<any>; +type TGitLabModule = TModule<typeof resources> & { + getUser: (id: string | number) => Promise<TGitLabUser>; + getProject: (id: string | number) => Promise<TGitLabProject>; // ... };I can generate a full typed surface if you confirm which type maps exist (Project, Group, Issue, etc.).
fync/src/gitlab/types/gitlab-merge-request.ts (2)
4-6: Remove duplicate id and specialize TBaseEntity for numeric IDsYou're declaring
id: numberand also intersecting withTBaseEntitywhich itself declares anid. Prefer specializing the base and letting it carry theidto avoid duplication and future drift.Apply:
-export type TGitLabMergeRequest = { - /** Merge request ID */ - id: number; +export type TGitLabMergeRequest = TBaseEntity<number> & { + /** Merge request internal ID */
16-20: Forward-compat for status unionsGitLab periodically adds new status values. Consider widening union types to accept unknown strings to prevent breaking consumers on minor API updates, while still guiding with known literals.
Example pattern:
- status: "created" | "waiting_for_resource" | "preparing" | "pending" | "running" | "success" | "failed" | "canceled" | "skipped" | "manual" | "scheduled"; + status: "created" | "waiting_for_resource" | "preparing" | "pending" | "running" | "success" | "failed" | "canceled" | "skipped" | "manual" | "scheduled" | (string & {});Apply similarly to
merge_status,detailed_merge_status, and pipeline/head_pipeline statuses.Also applies to: 112-125, 155-171
fync/src/gitlab/types/gitlab-pipeline.ts (3)
4-6: Remove duplicate id and specialize TBaseEntitySame pattern as MRs: avoid repeating the
idfield and specialize the base for numeric IDs.-export type TGitLabPipeline = { - /** Pipeline ID */ - id: number; +export type TGitLabPipeline = TBaseEntity<number> & { + /** Pipeline IID */
130-155: Reuse existing commit type to reduce duplication
TGitLabJob.commitduplicatesTGitLabMergeRequestCommitwith onlytrailersadded. Reuse the existing type and extend it locally.+import type { TGitLabMergeRequestCommit } from "./gitlab-merge-request"; @@ - /** Commit */ - commit: { - /** Commit ID */ - id: string; - /** Short ID */ - short_id: string; - /** Title */ - title: string; - /** Author name */ - author_name: string; - /** Author email */ - author_email: string; - /** Message */ - message: string; - /** Authored date */ - authored_date: string; - /** Committer name */ - committer_name: string; - /** Committer email */ - committer_email: string; - /** Committed date */ - committed_date: string; - /** Trailers */ - trailers: Record<string, string>; - /** Web URL */ - web_url: string; - }; + /** Commit */ + commit: TGitLabMergeRequestCommit & { + /** Trailers */ + trailers: Record<string, string>; + };This keeps one source of truth for commit fields and simplifies future updates.
Also applies to: 1-2
15-18: Optional: widen pipeline status/source unions for resilienceAs with MR statuses, consider allowing unknown strings to avoid breakage on new GitLab enum values.
- status: "created" | "waiting_for_resource" | "preparing" | "pending" | "running" | "success" | "failed" | "canceled" | "skipped" | "manual" | "scheduled"; + status: "created" | "waiting_for_resource" | "preparing" | "pending" | "running" | "success" | "failed" | "canceled" | "skipped" | "manual" | "scheduled" | (string & {}); @@ - source: "push" | "web" | "schedule" | "api" | "external" | "pipeline" | "chat" | "webide" | "merge_request_event" | "external_pull_request_event" | "parent_pipeline" | "ondemand_dast_scan" | "ondemand_dast_validation"; + source: "push" | "web" | "schedule" | "api" | "external" | "pipeline" | "chat" | "webide" | "merge_request_event" | "external_pull_request_event" | "parent_pipeline" | "ondemand_dast_scan" | "ondemand_dast_validation" | (string & {});fync/src/gitlab/types/gitlab-search.ts (1)
127-129: Scope completeness (optional)Depending on coverage targets, consider including additional scopes like
"epics"or"notes"if you plan to expose them soon.- scope?: "projects" | "issues" | "merge_requests" | "milestones" | "wiki_blobs" | "commits" | "blobs" | "users"; + scope?: "projects" | "issues" | "merge_requests" | "milestones" | "wiki_blobs" | "commits" | "blobs" | "users" | "epics" | "notes";fync/src/npm/types/npm-package.ts (1)
33-35: Prefer unknown over any and keep optionality accurateMinor polish: use
Record<string, unknown>instead ofany, and ensure optionality matches registry payloads.- config?: Record<string, any>; + config?: Record<string, unknown>; @@ - publishConfig?: Record<string, any>; + publishConfig?: Record<string, unknown>;Also applies to: 41-45
fync/src/gitlab/types/gitlab-issue.ts (13)
1-2: Double-check import path stability given PR’s export cleanupsPR touches core exports. Please verify that
../../core/typesstill re-exportsTBaseEntity. If not, import from the concrete file to avoid brittle barrels.Proposed change if the barrel no longer re-exports:
-import type { TBaseEntity } from "../../core/types"; +import type { TBaseEntity } from "../../core/types/base";
5-6: Avoid duplicateidtyping; parametrizeTBaseEntitywithnumber
id: numberis declared explicitly and also comes from& TBaseEntity(defaulting tostring | number). Intersecting works but is redundant and obscures intent. PreferTBaseEntity<number>and drop the explicitidproperty.export type TGitLabIssue = { - /** Issue ID */ - id: number; /** Issue internal ID */ iid: number; @@ -} & TBaseEntity; +} & TBaseEntity<number>;Also applies to: 172-172
175-176: Same duplication inTGitLabIssueNote; makeidcome fromTBaseEntity<number>Mirror the approach above for notes as well.
export type TGitLabIssueNote = { - /** Note ID */ - id: number; @@ -} & TBaseEntity; +} & TBaseEntity<number>;Also applies to: 236-236
3-3: Factor shared helper types at the topYou repeat the “references” shape and hardcode issue types inline. Introduce small helpers to dedupe and centralize these unions.
+// Shared helpers +type TGitLabReferences = { + short: string; + relative: string; + full: string; +}; + +// Align with GitLab v4 issue types (please confirm set below) +export type TGitLabIssueType = + | "issue" + | "incident" + | "test_case" + | "task"; +// NOTE: If "requirement" is truly returned in your payloads, extend the union here instead of inline.
24-24: Centralizeissue_typeunion and confirm valuesMove to
TGitLabIssueTypeand please confirm whether"requirement"is actually returned by your GitLab instance/API version. My understanding is that “requirement” is a separate entity rather than anissue_type.- issue_type: "issue" | "incident" | "test_case" | "requirement" | "task"; + issue_type: TGitLabIssueType;
104-111: DRY up duplicatereferencesstructuresUse the
TGitLabReferencesalias in both places./** References */ - references: { - /** Short reference */ - short: string; - /** Relative reference */ - relative: string; - /** Full reference */ - full: string; - }; + references: TGitLabReferences; @@ - /** Epic references */ - references: { - /** Short reference */ - short: string; - /** Relative reference */ - relative: string; - /** Full reference */ - full: string; - }; + /** Epic references */ + references: TGitLabReferences;Also applies to: 139-146
51-51: Verifymerge_requests_countfield; make optional if not always presentI don’t consistently see
merge_requests_counton issue payloads. If this isn’t guaranteed, mark as optional to avoid over-typing.- merge_requests_count: number; + merge_requests_count?: number;
67-76: Clarify time unit semantics intime_statsGitLab returns seconds for these numeric fields. Minor doc tweak helps consumers avoid misinterpretation.
- /** Time estimate */ - time_estimate: number; - /** Total time spent */ - total_time_spent: number; + /** Time estimate (seconds) */ + time_estimate: number; + /** Total time spent (seconds) */ + total_time_spent: number;
87-89: Consider narrowingtask_statusto the known values (if stable in your payloads)If your API responses are consistent, replace
stringwith a union of known statuses. Otherwise, leave asstring.- /** Task status */ - task_status: string; + /** Task status */ + task_status: + | "none" + | "todo" + | "incomplete" + | "done" + | (string & {}); // fallback for unknowns
188-188:noteable_typelikely needs"Commit"(and possibly"Epic") or the type name should narrow to Issue-onlyYou named the type
TGitLabIssueNotebut allow MR/Snippet noteables; either:
- Rename to
TGitLabNotefor general notes, or- Narrow to
"Issue"only.If keeping it general, add
"Commit"(common) and consider"Epic"depending on your scope.- noteable_type: "Issue" | "MergeRequest" | "Snippet"; + noteable_type: "Issue" | "MergeRequest" | "Snippet" | "Commit"; + // Add "Epic" here if you intend to cover group epics as noteables: + // | "Epic";
202-202: Broadentypefor notes to include"Note"(plain) and"LegacyDiffNote"Plain system/user notes often come as
"Note". Legacy diff notes can appear in older/compat modes.- type: "DiscussionNote" | "DiffNote" | null; + type: "Note" | "DiscussionNote" | "DiffNote" | "LegacyDiffNote" | null;
204-204:commands_changesshould be optionalIt’s only present when a quick action mutates state. Make it optional to reflect reality.
- /** Commands changes */ - commands_changes: { + /** Commands changes (present only when quick actions apply) */ + commands_changes?: {
233-233: Document timestamp formatMinor doc: specify ISO-8601 to help callers parse correctly.
- /** Spent at */ - spent_at: string; + /** Spent at (ISO-8601) */ + spent_at: string;
| export function [ApiName](config: { token: string }): T[ApiName]Module { | ||
| const base = build[ApiName](config, resources); | ||
| const [apiName] = base as T[ApiName]Module; | ||
|
|
||
| // Implement convenience methods | ||
| [apiName].getUser = function (id: string) { | ||
| return base.users.getUser({ id }); | ||
| }; | ||
|
|
||
| [apiName].searchItems = function (query: string, options?: any) { | ||
| return base.search.search({ | ||
| q: query, | ||
| limit: options?.limit || 20, | ||
| ...options, | ||
| }); | ||
| }; | ||
|
|
||
| return [apiName]; | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Template bug: invalid destructuring and identifier usage in Step 6.
const [apiName] = base as T[ApiName]Module; is not valid JavaScript/TypeScript. This will mislead contributors copying the template.
Apply:
-export function [ApiName](config: { token: string }): T[ApiName]Module {
- const base = build[ApiName](config, resources);
- const [apiName] = base as T[ApiName]Module;
+export function [ApiName](config: { token: string }): T[ApiName]Module {
+ const base = build[ApiName](config, resources);
+ const api = base as T[ApiName]Module;
// Implement convenience methods
- [apiName].getUser = function (id: string) {
+ api.getUser = function (id: string) {
return base.users.getUser({ id });
};
- [apiName].searchItems = function (query: string, options?: any) {
+ api.searchItems = function (query: string, options?: any) {
return base.search.search({
q: query,
limit: options?.limit || 20,
...options,
});
};
- return [apiName];
+ return api;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function [ApiName](config: { token: string }): T[ApiName]Module { | |
| const base = build[ApiName](config, resources); | |
| const [apiName] = base as T[ApiName]Module; | |
| // Implement convenience methods | |
| [apiName].getUser = function (id: string) { | |
| return base.users.getUser({ id }); | |
| }; | |
| [apiName].searchItems = function (query: string, options?: any) { | |
| return base.search.search({ | |
| q: query, | |
| limit: options?.limit || 20, | |
| ...options, | |
| }); | |
| }; | |
| return [apiName]; | |
| } | |
| ``` | |
| export function [ApiName](config: { token: string }): T[ApiName]Module { | |
| const base = build[ApiName](config, resources); | |
| const api = base as T[ApiName]Module; | |
| // Implement convenience methods | |
| api.getUser = function (id: string) { | |
| return base.users.getUser({ id }); | |
| }; | |
| api.searchItems = function (query: string, options?: any) { | |
| return base.search.search({ | |
| q: query, | |
| limit: options?.limit || 20, | |
| ...options, | |
| }); | |
| }; | |
| return api; | |
| } |
🤖 Prompt for AI Agents
.cursor/rules/fync-architecture.mdc around lines 122-141: the template uses
invalid destructuring and placeholder identifier syntax (`const [apiName] = base
as T[ApiName]Module;` and later `[apiName].getUser`) which is not valid JS/TS;
replace this with a real variable (e.g., const api = base as T[ApiName]Module;)
then attach the convenience methods to that variable (api.getUser = ...,
api.searchItems = ...) and return api; ensure all placeholder bracketed
identifiers are removed and replaced with the chosen variable name.
| ```typescript | ||
| { | ||
| path: "/users/{id}", | ||
| transform: (response: any) => ({ | ||
| id: response.user_id, | ||
| name: response.full_name, | ||
| email: response.email_address, | ||
| }) | ||
| } | ||
| ``` |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Rule conflict: arrow function used where arrows are prohibited.
The guide forbids arrow functions but the transform example uses one.
Apply:
{
path: "/users/{id}",
- transform: (response: any) => ({
- id: response.user_id,
- name: response.full_name,
- email: response.email_address,
- })
+ transform: function (response: any) {
+ return {
+ id: response.user_id,
+ name: response.full_name,
+ email: response.email_address,
+ };
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ```typescript | |
| { | |
| path: "/users/{id}", | |
| transform: (response: any) => ({ | |
| id: response.user_id, | |
| name: response.full_name, | |
| email: response.email_address, | |
| }) | |
| } | |
| ``` | |
| { | |
| path: "/users/{id}", | |
| transform: function (response: any) { | |
| return { | |
| id: response.user_id, | |
| name: response.full_name, | |
| email: response.email_address, | |
| }; | |
| } | |
| } |
🤖 Prompt for AI Agents
In .cursor/rules/fync-architecture.mdc around lines 309 to 318 the example uses
an arrow function for the transform property which conflicts with the guide;
replace the arrow with a regular function expression or declaration (e.g.,
transform: function(response: any) { return { id: response.user_id, name:
response.full_name, email: response.email_address }; }) making sure to preserve
types and returned object shape.
| ## Supported APIs | ||
|
|
||
| // Get user's playlists | ||
| const playlists = await spotify.me.playlists.get() | ||
|
|
||
| // Get user's saved tracks | ||
| const savedTracks = await spotify.me.tracks.get() | ||
| - **GitHub** - Repositories, users, issues, pull requests, actions, and more | ||
| - **Spotify** - Tracks, playlists, user profiles, playback control | ||
| - **NPM Registry** - Package information, versions, downloads, search | ||
| - **Google Calendar** - Events, calendars, scheduling | ||
| - **Google Drive** - Files, folders, sharing, metadata | ||
| - **Vercel** - Projects, deployments, analytics | ||
|
|
There was a problem hiding this comment.
Docs drift vs. PR objectives: fix factory import, add GitLab, update mutation signature, mention Spotify searchAll
- The PR removes the wildcard re-export of the enhanced factory and standardizes on the base client. The README still imports
createApiFactoryEnhancedfrom@remcostoeten/fync/core. - GitLab is now a supported API but is missing from the “Supported APIs” list.
- PR notes Calendar/Drive mutations now use
(data, options)with path placeholders; the Calendar example still uses a single-arg signature. - PR renames the Spotify convenience method
search→searchAll. Consider showcasing it alongside resource-scoped searches.
Proposed diffs:
Add GitLab to Supported APIs:
-## Supported APIs
+## Supported APIs
@@
- **Vercel** - Projects, deployments, analytics
+- **GitLab** - Projects, groups, issues, merge requests, pipelines, snippets, searchUse base factory instead of enhanced one:
-import { createApiFactoryEnhanced } from '@remcostoeten/fync/core'
+import { createFyncApi } from '@remcostoeten/fync/core'
@@
-const api = createApiFactoryEnhanced({
+const api = createFyncApi({
baseUrl: 'https://api.github.com',
useResult: true // Enable TResult pattern
})Update Calendar create mutation to pass path placeholders via options:
-const newEvent = await calendar.calendar('primary').events.create({
- summary: 'Meeting',
- start: { dateTime: '2024-01-15T10:00:00Z' },
- end: { dateTime: '2024-01-15T11:00:00Z' }
-})
+const newEvent = await calendar.calendar('primary').events.create(
+ {
+ summary: 'Meeting',
+ start: { dateTime: '2024-01-15T10:00:00Z' },
+ end: { dateTime: '2024-01-15T11:00:00Z' }
+ },
+ { path: { calendarId: 'primary' } } // (data, options) with path placeholders
+)Mention the renamed Spotify convenience method:
// Search and discovery
const tracks = await spotify.search.tracks('bohemian rhapsody')
const artists = await spotify.search.artists('queen')
+// Multi-scope convenience search across tracks, artists, albums, playlists
+const all = await spotify.searchAll('queen')If useResult is no longer controlled by createFyncApi, we should also revise that section—see next comment.
Also applies to: 167-185, 119-125, 78-81
🧰 Tools
🪛 LanguageTool
[grammar] ~21-~21: There might be a mistake here.
Context: ...issues, pull requests, actions, and more - Spotify - Tracks, playlists, user prof...
(QB_NEW_EN)
[grammar] ~22-~22: There might be a mistake here.
Context: ...aylists, user profiles, playback control - NPM Registry - Package information, ve...
(QB_NEW_EN)
[grammar] ~23-~23: There might be a mistake here.
Context: ...information, versions, downloads, search - Google Calendar - Events, calendars, s...
(QB_NEW_EN)
[grammar] ~24-~24: There might be a mistake here.
Context: ...lendar** - Events, calendars, scheduling - Google Drive - Files, folders, sharing...
(QB_NEW_EN)
[grammar] ~25-~25: There might be a mistake here.
Context: ...ve** - Files, folders, sharing, metadata - Vercel - Projects, deployments, analyt...
(QB_NEW_EN)
| ## Advanced Features | ||
|
|
||
| Fync provides a comprehensive error handling system with structured error information: | ||
| ### Error Handling with TResult Pattern | ||
|
|
||
| ```typescript | ||
| import { initializeErrorHandling, BaseError, HttpErrorHandler } from '@remcostoeten/fync/core' | ||
| import { createApiFactoryEnhanced } from '@remcostoeten/fync/core' | ||
|
|
||
| // Initialize error handling system | ||
| initializeErrorHandling() | ||
|
|
||
| // Use HTTP error handler with retry logic | ||
| const errorHandler = new HttpErrorHandler({ | ||
| maxAttempts: 3, | ||
| baseDelay: 1000, | ||
| backoffMultiplier: 2 | ||
| const api = createApiFactoryEnhanced({ | ||
| baseUrl: 'https://api.github.com', | ||
| useResult: true // Enable TResult pattern | ||
| }) | ||
|
|
||
| try { | ||
| const result = await errorHandler.execute( | ||
| async () => { | ||
| // Your API call here | ||
| return await api.getData() | ||
| }, | ||
| { | ||
| service: 'github', | ||
| endpoint: '/user', | ||
| method: 'GET' | ||
| } | ||
| ) | ||
| } catch (error) { | ||
| if (error instanceof BaseError) { | ||
| console.log('Error code:', error.info.code) | ||
| console.log('Service:', error.info.service) | ||
| console.log('Category:', error.info.category) | ||
| console.log('User message:', error.info.userMessage) | ||
| console.log('Suggested action:', error.info.suggestedAction) | ||
| console.log('Is retryable:', error.info.isRetryable) | ||
| } | ||
| const result = await api.get('/user') | ||
| if (result.success) { | ||
| console.log(result.data) | ||
| } else { | ||
| console.error(result.error) | ||
| } | ||
| ``` |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Confirm whether useResult is still supported and where TResult is exported
The PR states “Remove conflicting TResult re-export and keep canonical core/result.” If useResult is still a valid option on the base factory, great. If not, the example should be updated to show the current error-handling mechanism, and imports should come from @remcostoeten/fync/core/result.
Run:
Once confirmed, adjust the README imports and examples accordingly. I can send a follow-up patch.
🏁 Script executed:
#!/bin/bash
# Verify where TResult is exported from and whether useResult is supported
rg -nP 'export\s+(type|interface)\s+TResult' -C2 fync/src
rg -nP '\buseResult\b' -C2 fync/src
# Check any re-exports at core/index.ts
rg -nP 'export\s+\*.*api-factory-enhanced' fync/src/core/index.ts -C2Length of output: 399
🏁 Script executed:
#!/bin/bash
# Locate the definition of createApiFactoryEnhanced to inspect its parameters
rg -nP -n 'export\s+(async\s+)?function\s+createApiFactoryEnhanced' -C5 fync/src
rg -nP -n 'createApiFactoryEnhanced\<\' -C5 fync/srcLength of output: 202
🏁 Script executed:
#!/bin/bash
# Locate all occurrences of createApiFactoryEnhanced to find its definition and usage
rg -n 'createApiFactoryEnhanced' -C5 fync/src
# Inspect core/index.ts to see which symbols are re-exported
sed -n '1,200p' fync/src/core/index.tsLength of output: 414
Update README code snippet to reflect current API and TResult export
The useResult option and the createApiFactoryEnhanced export have been removed in favor of the core factory returning TResult by default, and the canonical TResult/TApiError types now live in core/result.ts. Please update fync/README.md as follows:
- Replace the import of
createApiFactoryEnhancedwith the basecreateApiFactory. - Remove the
useResult: trueoption (the factory returns aTResult–wrapped response by default). - Import
TResultandTApiErrorfrom@remcostoeten/fync/core/resultinstead of relying on a conflicting re-export. - Adjust the example to include generics on the request and illustrate the new error-handling pattern.
Example revised snippet:
- import { createApiFactoryEnhanced } from '@remcostoeten/fync/core'
+ import { createApiFactory } from '@remcostoeten/fync/core'
+ import { TResult, TApiError } from '@remcostoeten/fync/core/result'
- const api = createApiFactoryEnhanced({
- baseUrl: 'https://api.github.com',
- useResult: true // Enable TResult pattern
- })
+ const api = createApiFactory({
+ baseUrl: 'https://api.github.com'
+ })
- const result = await api.get('/user')
+ // Specify the expected data type; TResult<User, TApiError> is returned automatically
+ type User = { login: string; /* … */ }
+ const result: TResult<User, TApiError> = await api.get<User>('/user')
if (result.success) {
console.log(result.data)
} else {
console.error(result.error)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ## Advanced Features | |
| Fync provides a comprehensive error handling system with structured error information: | |
| ### Error Handling with TResult Pattern | |
| ```typescript | |
| import { initializeErrorHandling, BaseError, HttpErrorHandler } from '@remcostoeten/fync/core' | |
| import { createApiFactoryEnhanced } from '@remcostoeten/fync/core' | |
| // Initialize error handling system | |
| initializeErrorHandling() | |
| // Use HTTP error handler with retry logic | |
| const errorHandler = new HttpErrorHandler({ | |
| maxAttempts: 3, | |
| baseDelay: 1000, | |
| backoffMultiplier: 2 | |
| const api = createApiFactoryEnhanced({ | |
| baseUrl: 'https://api.github.com', | |
| useResult: true // Enable TResult pattern | |
| }) | |
| try { | |
| const result = await errorHandler.execute( | |
| async () => { | |
| // Your API call here | |
| return await api.getData() | |
| }, | |
| { | |
| service: 'github', | |
| endpoint: '/user', | |
| method: 'GET' | |
| } | |
| ) | |
| } catch (error) { | |
| if (error instanceof BaseError) { | |
| console.log('Error code:', error.info.code) | |
| console.log('Service:', error.info.service) | |
| console.log('Category:', error.info.category) | |
| console.log('User message:', error.info.userMessage) | |
| console.log('Suggested action:', error.info.suggestedAction) | |
| console.log('Is retryable:', error.info.isRetryable) | |
| } | |
| const result = await api.get('/user') | |
| if (result.success) { | |
| console.log(result.data) | |
| } else { | |
| console.error(result.error) | |
| } | |
| ``` | |
| import { createApiFactory } from '@remcostoeten/fync/core' | |
| import { TResult, TApiError } from '@remcostoeten/fync/core/result' | |
| const api = createApiFactory({ | |
| baseUrl: 'https://api.github.com' | |
| }) | |
| // Specify the expected data type; TResult<User, TApiError> is returned automatically | |
| type User = { login: string; /* … */ } | |
| const result: TResult<User, TApiError> = await api.get<User>('/user') | |
| if (result.success) { | |
| console.log(result.data) | |
| } else { | |
| console.error(result.error) | |
| } |
🤖 Prompt for AI Agents
In fync/README.md around lines 167–185, the example uses the removed
createApiFactoryEnhanced export and the obsolete useResult option and doesn't
import TResult/TApiError from the new location; update the snippet to import
createApiFactory from '@remcostoeten/fync/core', remove the useResult option
(the factory returns TResult by default), import TResult and TApiError from
'@remcostoeten/fync/core/result', and adjust the example to use generics on the
request (e.g., TResult<MyData, TApiError>) and show checking result.success then
using result.data or logging result.error to illustrate the new TResult-based
error-handling pattern.
| 1. **Do not** open a public GitHub issue | ||
| 2. Email security concerns to: [your-email@domain.com] | ||
| 3. Include detailed information about the vulnerability | ||
| 4. Allow reasonable time for us to address the issue before public disclosure | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Replace placeholder security contacts with real channels and define an SLA.
Responsible disclosure needs a working contact and an acknowledgement/response timeline. The placeholders make the policy non-actionable.
Apply this diff as a starting point (adjust to your actual domain/contact):
-1. **Do not** open a public GitHub issue
-2. Email security concerns to: [your-email@domain.com]
+1. **Do not** open a public GitHub issue
+2. Email security concerns to: security@fync.dev
3. Include detailed information about the vulnerability
4. Allow reasonable time for us to address the issue before public disclosure-For security-related questions or concerns:
-- Email: [your-security-email@domain.com]
+For security-related questions or concerns:
+- Email: security@fync.dev
- GitHub: [@remcostoeten](https://github.com/remcostoeten)Optionally add a security.txt at /.well-known/security.txt and link to it here. Do you want me to draft that file?
Also applies to: 175-179
🤖 Prompt for AI Agents
In fync/SECURITY.md around lines 7-11 (and similarly update lines 175-179),
replace the placeholder email with your real security contact(s) (e.g.,
security@yourdomain.com and an encrypted PGP key or vulnerability disclosure
team alias), add a clear SLA: an acknowledgement timeframe (e.g., within 48
hours) and a remediation/response timeline (e.g., initial triage within 5
business days and periodic updates), list alternative contact channels (PGP
fingerprint, web form, or a ticket URL), and optionally note that a security.txt
is served at /.well-known/security.txt with a link; ensure the wording is
actionable and non-placeholder.
| @@ -0,0 +1,273 @@ | |||
| import type { TBaseEntity } from "../../core/types"; | |||
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Confirm TBaseEntity export path
Same note as in MR types: verify ../../core/types actually exports TBaseEntity.
Run:
🏁 Script executed:
#!/bin/bash
fd -a 'core/types/index.ts'
rg -n 'export .* TBaseEntity' fync/src/core/types -C2Length of output: 340
Incorrect import path for TBaseEntity
The module fync/src/core/types does not export TBaseEntity directly (there’s no index.ts barrel file). The only declaration is in core/types/base.ts, so importing from "../../core/types" will fail at compile time.
• Location needing update:
- File
fync/src/gitlab/types/gitlab-pipeline.ts, line 1 - Current:
import type { TBaseEntity } from "../../core/types";
- Should be either:
import type { TBaseEntity } from "../../core/types/base";
– Or add a new fync/src/core/types/index.ts that re-exports TBaseEntity from base.ts if you prefer a barrel file.
🤖 Prompt for AI Agents
In fync/src/gitlab/types/gitlab-pipeline.ts at line 1, the import path "import
type { TBaseEntity } from \"../../core/types\";" is incorrect because
TBaseEntity is declared in core/types/base.ts; update the import to reference
the concrete file path "../../core/types/base" or alternatively add a new barrel
file fync/src/core/types/index.ts that re-exports TBaseEntity from base.ts and
then keep the original import. Ensure the chosen fix is applied consistently
across imports that reference "../../core/types".
| /** Number of followers */ | ||
| followers?: number; | ||
| /** Number of users following */ | ||
| following?: number; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Remove GitHub-specific fields from TGitLabUser
followers, following, and public_repos don't exist on GitLab v4 user payloads. Keeping them will mislead consumers and break type-level guarantees.
Apply this diff:
/** Number of followers */
- followers?: number;
/** Number of users following */
- following?: number;
@@
- /** Number of public projects */
- public_repos?: number;
+ /** Public email */
+ public_email?: string | null;Also applies to: 55-57
🌐 Web query:
GitLab API v4 "Users" response fields list (verify whether `followers`, `following`, and `public_repos` exist; confirm `public_email`).
💡 Result:
Short answer: GitLab API v4 user objects do NOT include followers, following, or public_repos. They do include public_email (named public_email in the user object). [1][2]
Supporting cites:
- Official GitLab API Users docs (response fields such as id, username, name, state, avatar_url, web_url, created_at, bio, location, public_email, etc.). No followers/following/public_repos fields are listed. [1]
- Example GET /users/:id response in the docs shows public_email and other profile fields; no followers/following/public_repos present. [2]
References:
- https://docs.gitlab.com/ee/api/users.html
- https://docs.gitlab.com/ee/api/users.html#single-user (example response)
Remove GitHub-specific fields from TGitLabUser
GitLab API v4 user objects do not include followers, following, or public_repos, but they do include public_email. Keeping the GitHub-specific fields will mislead consumers and break type-level guarantees.
Apply this diff:
/** Number of followers */
- followers?: number;
/** Number of users following */
- following?: number;
@@
- /** Number of public projects */
- public_repos?: number;
+ /** Public email */
+ public_email?: string | null;Also applies to lines 55–57.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** Number of followers */ | |
| followers?: number; | |
| /** Number of users following */ | |
| following?: number; | |
| // --- around lines 38–41 in fync/src/gitlab/types/gitlab-user.ts --- | |
| /** Number of followers */ | |
| /** Number of users following */ | |
| // --- around lines 55–57 in fync/src/gitlab/types/gitlab-user.ts --- | |
| /** Public email */ | |
| public_email?: string | null; |
🤖 Prompt for AI Agents
In fync/src/gitlab/types/gitlab-user.ts around lines 38–41 and 55–57, the type
includes GitHub-specific fields (followers, following, public_repos) that do not
exist on GitLab API v4 user objects; remove those fields and replace them with
GitLab-appropriate properties (add public_email?: string) so the TGitLabUser
type matches GitLab v4 schema and avoids misleading consumers and type breaks.
| @@ -1,55 +1,68 @@ | |||
| import type { TBaseEntity } from "../../core/types"; | |||
There was a problem hiding this comment.
Fix impossible/incorrect intersections with TBaseEntity across NPM types
Several NPM types intersect with TBaseEntity<...> which injects a required id field and timestamp shape that the NPM registry responses do not have. In one case (TNpmVulnerability) this creates an unsatisfiable type: id: string (from base) ∩ id: number (local) ⇒ never.
This will confuse consumers and can silently break type safety. Remove TBaseEntity from NPM shapes and keep the actual fields (_id, _rev, etc.). Also, make files optional; it’s not guaranteed in registry metadata.
Apply:
-import type { TBaseEntity } from "../../core/types";
@@
-type TNpmPackageInfo = TBaseEntity<string> & {
+type TNpmPackageInfo = {
_id: string;
_rev: string;
@@
-};
+};
@@
-type TNpmDownloadsPoint = TBaseEntity<string> & {
+type TNpmDownloadsPoint = {
downloads: number;
day: string;
};
@@
-type TNpmDownloadsResponse = TBaseEntity<string> & {
+type TNpmDownloadsResponse = {
downloads: TNpmDownloadsPoint[];
start: string;
end: string;
package: string;
};
@@
-type TNpmDownloadCount = TBaseEntity<string> & {
+type TNpmDownloadCount = {
downloads: number;
start: string;
end: string;
package: string;
};
@@
-type TNpmUser = TBaseEntity<string> & {
+type TNpmUser = {
_id: string;
_rev: string;
@@
-};
+};
@@
-type TNpmOrganization = TBaseEntity<string> & {
+type TNpmOrganization = {
_id: string;
_rev: string;
@@
-};
+};
@@
-type TNpmVulnerability = TBaseEntity<string> & {
+type TNpmVulnerability = {
id: number;
@@
-};
+};
@@
- files: string[];
+ files?: string[];If you intended to share timestamp fields, introduce a separate TTimestamps-compatible shape with NPM’s field names (e.g., created, updated) rather than reusing TBaseEntity.
Also applies to: 66-85, 125-136, 137-143, 144-157, 158-167, 168-185
| export function [ApiName](config: { token: string }): T[ApiName]Module { | ||
| const base = build[ApiName](config, resources); | ||
| const [apiName] = base as T[ApiName]Module; | ||
|
|
||
| // Implement convenience methods | ||
| [apiName].getUser = function (id: string) { | ||
| return base.users.getUser({ id }); | ||
| }; | ||
|
|
||
| [apiName].searchItems = function (query: string, options?: any) { | ||
| return base.search.search({ | ||
| q: query, | ||
| limit: options?.limit || 20, | ||
| ...options, | ||
| }); | ||
| }; | ||
|
|
||
| return [apiName]; | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Fix invalid template code (destructuring and return value are wrong)
const [apiName] = base as T[ApiName]Module; is invalid, and returning [apiName] is incorrect. This snippet will confuse adopters.
Apply this diff:
-export function [ApiName](config: { token: string }): T[ApiName]Module {
- const base = build[ApiName](config, resources);
- const [apiName] = base as T[ApiName]Module;
-
- // Implement convenience methods
- [apiName].getUser = function (id: string) {
- return base.users.getUser({ id });
- };
-
- [apiName].searchItems = function (query: string, options?: any) {
- return base.search.search({
- q: query,
- limit: options?.limit || 20,
- ...options,
- });
- };
-
- return [apiName];
-}
+export function [ApiName](config: { token: string }): T[ApiName]Module {
+ const base = build[ApiName](config, resources) as T[ApiName]Module;
+
+ // Implement convenience methods
+ base.getUser = function (id: string) {
+ return base.users.getUser({ id });
+ };
+
+ base.searchItems = function (query: string, options?: any) {
+ return base.search.search({
+ q: query,
+ limit: options?.limit ?? 20,
+ ...options,
+ });
+ };
+
+ return base;
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function [ApiName](config: { token: string }): T[ApiName]Module { | |
| const base = build[ApiName](config, resources); | |
| const [apiName] = base as T[ApiName]Module; | |
| // Implement convenience methods | |
| [apiName].getUser = function (id: string) { | |
| return base.users.getUser({ id }); | |
| }; | |
| [apiName].searchItems = function (query: string, options?: any) { | |
| return base.search.search({ | |
| q: query, | |
| limit: options?.limit || 20, | |
| ...options, | |
| }); | |
| }; | |
| return [apiName]; | |
| } | |
| ``` | |
| export function [ApiName](config: { token: string }): T[ApiName]Module { | |
| const base = build[ApiName](config, resources) as T[ApiName]Module; | |
| // Implement convenience methods | |
| base.getUser = function (id: string) { | |
| return base.users.getUser({ id }); | |
| }; | |
| base.searchItems = function (query: string, options?: any) { | |
| return base.search.search({ | |
| q: query, | |
| limit: options?.limit ?? 20, | |
| ...options, | |
| }); | |
| }; | |
| return base; | |
| } |
🤖 Prompt for AI Agents
In GEMINI.md around lines 120 to 139, the snippet incorrectly destructures and
returns the module: replace "const [apiName] = base as T[ApiName]Module;" with a
single identifier binding like "const api = base as T[ApiName]Module;" then
attach convenience methods to that identifier (e.g., api.getUser = ...
api.searchItems = ...) and finally return that identifier (return api); ensure
you do not use array/destructuring syntax and that the returned value is the
module object.
| export function [ApiName](config: { token: string }): T[ApiName]Module { | ||
| const base = build[ApiName](config, resources); | ||
| const [apiName] = base as T[ApiName]Module; | ||
|
|
||
| // Implement convenience methods | ||
| [apiName].getExample = function (id: string) { | ||
| return base.examples.getExample({ id }); | ||
| }; | ||
|
|
||
| return [apiName]; | ||
| } |
There was a problem hiding this comment.
Same bug in the full template block
Mirror the above fix in the “Complete Implementation Template” section to keep guidance consistent and correct.
-export function [ApiName](config: { token: string }): T[ApiName]Module {
- const base = build[ApiName](config, resources);
- const [apiName] = base as T[ApiName]Module;
-
- // Implement convenience methods
- [apiName].getExample = function (id: string) {
- return base.examples.getExample({ id });
- };
-
- return [apiName];
-}
+export function [ApiName](config: { token: string }): T[ApiName]Module {
+ const base = build[ApiName](config, resources) as T[ApiName]Module;
+
+ // Implement convenience methods
+ base.getExample = function (id: string) {
+ return base.examples.getExample({ id });
+ };
+
+ return base;
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function [ApiName](config: { token: string }): T[ApiName]Module { | |
| const base = build[ApiName](config, resources); | |
| const [apiName] = base as T[ApiName]Module; | |
| // Implement convenience methods | |
| [apiName].getExample = function (id: string) { | |
| return base.examples.getExample({ id }); | |
| }; | |
| return [apiName]; | |
| } | |
| export function [ApiName](config: { token: string }): T[ApiName]Module { | |
| const base = build[ApiName](config, resources) as T[ApiName]Module; | |
| // Implement convenience methods | |
| base.getExample = function (id: string) { | |
| return base.examples.getExample({ id }); | |
| }; | |
| return base; | |
| } |
🤖 Prompt for AI Agents
In GEMINI.md around lines 182–192, the “Complete Implementation Template”
incorrectly destructures and returns [apiName] instead of working with the built
module; update the snippet to cast base to the module type (e.g., const
apiModule = base as T[ApiName]Module), attach convenience methods to apiModule
(apiModule.getExample = function(id: string) { return
apiModule.examples.getExample({ id }); }), and return apiModule so the template
mirrors the earlier fixed example.
This PR addresses the audit items:
Typecheck passes with npx tsc --noEmit.
Follow-up: We can re-introduce the enhanced client behind a separate named export with non-conflicting types if desired.
Summary by CodeRabbit