-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
42 changed files
with
610 additions
and
342 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,20 @@ | ||
# @xata.io/cli | ||
|
||
## 0.15.0 | ||
|
||
### Minor Changes | ||
|
||
- [#1251](https://github.com/xataio/client-ts/pull/1251) [`e97d1999`](https://github.com/xataio/client-ts/commit/e97d1999f3c25f149213ceca81958e1674624e05) Thanks [@SferaDev](https://github.com/SferaDev)! - Remove Xata Workers support | ||
|
||
### Patch Changes | ||
|
||
- [#1212](https://github.com/xataio/client-ts/pull/1212) [`1348a7fa`](https://github.com/xataio/client-ts/commit/1348a7fa973d8ad008f925297a479f26d231efec) Thanks [@eemmiillyy](https://github.com/eemmiillyy)! - feat: compatibility endpoint | ||
|
||
- Updated dependencies [[`e97d1999`](https://github.com/xataio/client-ts/commit/e97d1999f3c25f149213ceca81958e1674624e05)]: | ||
- @xata.io/[email protected] | ||
- @xata.io/[email protected] | ||
- @xata.io/[email protected] | ||
|
||
## 0.14.5 | ||
|
||
### Patch Changes | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import { writeFile, readFile, stat } from 'fs/promises'; | ||
import fetch from 'node-fetch'; | ||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; | ||
import { ONE_DAY, check, fetchInfo, getSdkVersion } from './compatibility.js'; | ||
import semver from 'semver'; | ||
|
||
vi.mock('node-fetch'); | ||
vi.mock('fs/promises'); | ||
|
||
afterEach(() => { | ||
vi.clearAllMocks(); | ||
}); | ||
|
||
const fetchMock = fetch as unknown as ReturnType<typeof vi.fn>; | ||
const writeFileMock = writeFile as unknown as ReturnType<typeof vi.fn>; | ||
const readFileMock = readFile as unknown as ReturnType<typeof vi.fn>; | ||
const statMock = stat as unknown as ReturnType<typeof vi.fn>; | ||
|
||
const latestAvailableVersionCLI = '1.0.0'; | ||
const latestAvailableVersionSDK = '2.0.0'; | ||
const specificVersionCLI = '0.0.8'; | ||
|
||
const userVersionCLI = '~0.0.1'; | ||
const userVersionSDK = '^0.0.2'; | ||
const userVersionAlpha = `${latestAvailableVersionCLI}-alpha.v927d47c`; | ||
|
||
const cliUpdateAvailable = `"✨ A newer version of @xata.io/cli is now available: ${latestAvailableVersionCLI}. You are currently using version: ${semver.coerce( | ||
userVersionCLI | ||
)}"`; | ||
const sdkUpdateAvailable = `"✨ A newer version of @xata.io/client is now available: ${latestAvailableVersionSDK}. You are currently using version: ${semver.coerce( | ||
userVersionSDK | ||
)}"`; | ||
|
||
const cliError = `"Incompatible version of @xata.io/cli: ${semver.coerce( | ||
userVersionCLI | ||
)}. Please upgrade to a version that satisfies: >=${latestAvailableVersionCLI}||${specificVersionCLI}."`; | ||
const sdkError = `"Incompatible version of @xata.io/client: ${semver.coerce( | ||
userVersionSDK | ||
)}. Please upgrade to a version that satisfies: ${latestAvailableVersionSDK}."`; | ||
|
||
const compatibilityFile = './compatibility.json'; | ||
|
||
const compat = { | ||
'@xata.io/cli': { | ||
latest: latestAvailableVersionCLI, | ||
compatibility: [ | ||
{ | ||
range: `>=${latestAvailableVersionCLI}` | ||
}, | ||
{ | ||
range: `${specificVersionCLI}` | ||
} | ||
] | ||
}, | ||
'@xata.io/client': { | ||
latest: latestAvailableVersionSDK, | ||
compatibility: [ | ||
{ | ||
range: latestAvailableVersionSDK | ||
} | ||
] | ||
} | ||
}; | ||
|
||
const packageJsonObj = (withPackage: boolean) => { | ||
return { | ||
name: 'client-ts', | ||
dependencies: withPackage | ||
? { | ||
'@xata.io/client': userVersionSDK | ||
} | ||
: {} | ||
}; | ||
}; | ||
|
||
fetchMock.mockReturnValue({ ok: true, json: async () => compat }); | ||
|
||
describe('getSdkVersion', () => { | ||
test('returns version when @xata package', async () => { | ||
readFileMock.mockReturnValue(JSON.stringify(packageJsonObj(true))); | ||
expect(await getSdkVersion()).toEqual(userVersionSDK); | ||
}); | ||
|
||
test('returns null when no @xata package', async () => { | ||
readFileMock.mockReturnValue(JSON.stringify(packageJsonObj(false))); | ||
expect(await getSdkVersion()).toEqual(null); | ||
}); | ||
}); | ||
|
||
describe('fetchInfo', () => { | ||
const fetchInfoParams = { file: compatibilityFile, url: '' }; | ||
|
||
describe('refreshes', () => { | ||
beforeEach(() => { | ||
readFileMock.mockReturnValue(JSON.stringify(compat)); | ||
writeFileMock.mockReturnValue(undefined); | ||
}); | ||
|
||
test('when no files exist', async () => { | ||
statMock.mockRejectedValue(undefined); | ||
|
||
await fetchInfo(fetchInfoParams); | ||
expect(writeFileMock).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
test('when file is stale', async () => { | ||
const yesterday = new Date().getDate() - ONE_DAY + 1000; | ||
statMock.mockReturnValue({ mtime: new Date(yesterday) }); | ||
|
||
await fetchInfo(fetchInfoParams); | ||
expect(writeFileMock).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
test('when problem fetching, no file writes', async () => { | ||
fetchMock.mockReturnValue({ | ||
ok: false | ||
}); | ||
|
||
const yesterday = new Date().getDate() - ONE_DAY + 1000; | ||
statMock.mockReturnValue({ mtime: new Date(yesterday) }); | ||
|
||
await fetchInfo(fetchInfoParams); | ||
expect(writeFileMock).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe('does not refresh', () => { | ||
test('if file is not stale', async () => { | ||
statMock.mockReturnValue({ mtime: new Date() }); | ||
|
||
await fetchInfo(fetchInfoParams); | ||
expect(fetchMock).not.toHaveBeenCalled(); | ||
expect(writeFileMock).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('checks', () => { | ||
describe('latest', () => { | ||
beforeEach(() => { | ||
readFileMock.mockReturnValue(JSON.stringify(compat)); | ||
}); | ||
|
||
test('returns warn if newer package available', async () => { | ||
const cliResponse = await check({ compat, pkg: '@xata.io/cli', version: userVersionCLI }); | ||
expect(cliResponse.warn).toMatchInlineSnapshot(cliUpdateAvailable); | ||
|
||
const sdkResponse = await check({ compat, pkg: '@xata.io/client', version: userVersionSDK }); | ||
expect(sdkResponse.warn).toMatchInlineSnapshot(sdkUpdateAvailable); | ||
}); | ||
|
||
test('returns null if no newer package available', async () => { | ||
const cliResponse = await check({ compat, pkg: '@xata.io/cli', version: latestAvailableVersionCLI }); | ||
expect(cliResponse.warn).toBeNull(); | ||
|
||
const sdkResponse = await check({ compat, pkg: '@xata.io/client', version: latestAvailableVersionSDK }); | ||
expect(sdkResponse.warn).toBeNull(); | ||
}); | ||
}); | ||
|
||
describe('compatibility', () => { | ||
beforeEach(() => { | ||
readFileMock.mockReturnValue(JSON.stringify(compat)); | ||
}); | ||
|
||
test('returns error if not compatible', async () => { | ||
const cliResponse = await check({ compat, pkg: '@xata.io/cli', version: userVersionCLI }); | ||
expect(cliResponse.error).toMatchInlineSnapshot(cliError); | ||
|
||
const sdkResponse = await check({ compat, pkg: '@xata.io/client', version: userVersionSDK }); | ||
expect(sdkResponse.error).toMatchInlineSnapshot(sdkError); | ||
}); | ||
|
||
test('returns null if compatible', async () => { | ||
const cliResponse = await check({ compat, pkg: '@xata.io/cli', version: latestAvailableVersionCLI }); | ||
expect(cliResponse.error).toBeNull(); | ||
|
||
const sdkResponse = await check({ compat, pkg: '@xata.io/client', version: latestAvailableVersionSDK }); | ||
expect(sdkResponse.error).toBeNull(); | ||
|
||
// Alpha versions | ||
const cliResponseAlpha = await check({ compat, pkg: '@xata.io/cli', version: userVersionAlpha }); | ||
expect(cliResponseAlpha.error).toBeNull(); | ||
expect(cliResponseAlpha.warn).toBeNull(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { Hook } from '@oclif/core'; | ||
import { readFile, stat, writeFile, mkdir } from 'fs/promises'; | ||
import semver from 'semver'; | ||
import path from 'path'; | ||
import fetch from 'node-fetch'; | ||
|
||
export const ONE_DAY = 1000 * 60 * 60 * 24 * 1; | ||
|
||
export type Package = '@xata.io/cli' | '@xata.io/client'; | ||
export type Compatibility = Record<Package, { latest: string; compatibility: { range: string }[] }>; | ||
export type PackageJson = { dependencies: Record<string, string> }; | ||
|
||
export const check = async ({ pkg, version, compat }: { pkg: Package; version: string; compat: Compatibility }) => { | ||
const currentVersion = semver.coerce(version)?.version as string; | ||
const updateAvailable = semver.lt(currentVersion, compat[pkg].latest); | ||
const compatibleRange = compat[pkg].compatibility.map((v) => v.range).join('||'); | ||
const semverCompatible = semver.satisfies(currentVersion, compatibleRange); | ||
|
||
return { | ||
warn: updateAvailable | ||
? `✨ A newer version of ${pkg} is now available: ${compat[pkg].latest}. You are currently using version: ${currentVersion}` | ||
: null, | ||
error: !semverCompatible | ||
? `Incompatible version of ${pkg}: ${currentVersion}. Please upgrade to a version that satisfies: ${compatibleRange}.` | ||
: null | ||
}; | ||
}; | ||
|
||
export const getSdkVersion = async (): Promise<null | string> => { | ||
const packageJson: PackageJson = JSON.parse(await readFile(`${path.join(process.cwd())}/package.json`, 'utf-8')); | ||
return packageJson?.dependencies?.['@xata.io/client'] ? packageJson.dependencies['@xata.io/client'] : null; | ||
}; | ||
|
||
export const fetchInfo = async ({ url, file }: { url: string; file: string }) => { | ||
let shouldRefresh = true; | ||
|
||
try { | ||
// Latest time of one of the files should be enough | ||
const statResult = await stat(file); | ||
const lastModified = new Date(statResult.mtime); | ||
// Last param is the number of days - we fetch new package info if the file is older than 1 day | ||
const staleAt = new Date(lastModified.valueOf() + ONE_DAY); | ||
shouldRefresh = new Date() > staleAt; | ||
} catch (error) { | ||
// Do nothing | ||
} | ||
|
||
if (shouldRefresh) { | ||
try { | ||
const latestCompatibilityResponse = await fetch(url); | ||
if (!latestCompatibilityResponse.ok) return; | ||
const body = await latestCompatibilityResponse.json(); | ||
if (!(body as Compatibility)['@xata.io/cli']) return; | ||
|
||
try { | ||
await writeFile(file, JSON.stringify(body)); | ||
} catch (error: any) { | ||
if (error.code === 'ENOENT') { | ||
await mkdir(path.dirname(file), { recursive: true }); | ||
await writeFile(file, JSON.stringify(body)); | ||
} | ||
} | ||
} catch (error) { | ||
// Do nothing | ||
} | ||
} | ||
}; | ||
|
||
const hook: Hook<'init'> = async function (_options) { | ||
const dir = path.join(process.cwd(), '.xata', 'version'); | ||
const compatibilityFile = `${dir}/compatibility.json`; | ||
const compatibilityUri = 'https://raw.githubusercontent.com/xataio/client-ts/main/compatibility.json'; | ||
|
||
const displayWarning = async () => { | ||
const compat: Compatibility = JSON.parse(await readFile(compatibilityFile, 'utf-8')); | ||
|
||
const checks = [ | ||
{ pkg: '@xata.io/cli', version: this.config.version }, | ||
{ pkg: '@xata.io/client', version: await getSdkVersion() } | ||
] as const; | ||
|
||
for (const { pkg, version } of checks) { | ||
if (!version) continue; | ||
|
||
const { warn, error } = await check({ pkg, version, compat }); | ||
if (warn) this.log(warn); | ||
if (error) this.error(error); | ||
} | ||
}; | ||
|
||
await fetchInfo({ file: compatibilityFile, url: compatibilityUri }); | ||
await displayWarning(); | ||
}; | ||
|
||
export default hook; |
Oops, something went wrong.