backless-google-drive 0.9.6 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -8,6 +8,14 @@ declare class GoogleDriveApi implements CloudStorageApi {
8
8
  private request;
9
9
  getOrCreateAppFolder(name: string): Promise<string>;
10
10
  ensureFolder(parentId: string, name: string): Promise<string>;
11
+ private matchingFolders;
12
+ private createFolder;
13
+ /**
14
+ * Deterministic across clients: oldest by createdTime, id as a stable tiebreak.
15
+ * A missing/blank createdTime sorts last (treated as newest) so a folder with a real
16
+ * timestamp always wins over one whose creation time the API happened to omit.
17
+ */
18
+ private oldestFolderId;
11
19
  listFiles(folderId: string): Promise<DriveFile[]>;
12
20
  /**
13
21
  * Batched form of {@link listFiles}: one query per chunk of folders instead of
package/dist/index.js CHANGED
@@ -52,29 +52,48 @@ var GoogleDriveApi = class {
52
52
  const cacheKey = `${parentId}:${name}`;
53
53
  const cached = this.folderCache.get(cacheKey);
54
54
  if (cached) return cached;
55
+ let folderId = this.oldestFolderId(await this.matchingFolders(parentId, name));
56
+ if (!folderId) {
57
+ const created = await this.createFolder(parentId, name);
58
+ folderId = this.oldestFolderId(await this.matchingFolders(parentId, name)) ?? created;
59
+ }
60
+ this.folderCache.set(cacheKey, folderId);
61
+ return folderId;
62
+ }
63
+ async matchingFolders(parentId, name) {
55
64
  const q = `'${parentId}' in parents and name = '${escapeDriveQuery(name)}' and mimeType = '${FOLDER_MIME}' and trashed = false`;
56
65
  const resp = await this.request(
57
- `${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=files(id,name)&spaces=drive`
66
+ `${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=files(id,name,createdTime)&spaces=drive`
58
67
  );
59
68
  const data = await resp.json();
60
- let folderId;
61
- if (data.files?.length > 0) {
62
- folderId = data.files[0].id;
63
- } else {
64
- const createResp = await this.request(`${DRIVE_API}/files`, {
65
- method: "POST",
66
- headers: { "Content-Type": "application/json" },
67
- body: JSON.stringify({
68
- name,
69
- mimeType: FOLDER_MIME,
70
- parents: [parentId]
71
- })
72
- });
73
- const created = await createResp.json();
74
- folderId = created.id;
75
- }
76
- this.folderCache.set(cacheKey, folderId);
77
- return folderId;
69
+ return data.files ?? [];
70
+ }
71
+ async createFolder(parentId, name) {
72
+ const createResp = await this.request(`${DRIVE_API}/files`, {
73
+ method: "POST",
74
+ headers: { "Content-Type": "application/json" },
75
+ body: JSON.stringify({ name, mimeType: FOLDER_MIME, parents: [parentId] })
76
+ });
77
+ const created = await createResp.json();
78
+ return created.id;
79
+ }
80
+ /**
81
+ * Deterministic across clients: oldest by createdTime, id as a stable tiebreak.
82
+ * A missing/blank createdTime sorts last (treated as newest) so a folder with a real
83
+ * timestamp always wins over one whose creation time the API happened to omit.
84
+ */
85
+ oldestFolderId(files) {
86
+ if (files.length === 0) return void 0;
87
+ return [...files].sort((a, b) => {
88
+ const at = a.createdTime;
89
+ const bt = b.createdTime;
90
+ if (at !== bt) {
91
+ if (!at) return 1;
92
+ if (!bt) return -1;
93
+ return at < bt ? -1 : 1;
94
+ }
95
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
96
+ })[0].id;
78
97
  }
79
98
  async listFiles(folderId) {
80
99
  const q = `'${folderId}' in parents and trashed = false`;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/google-drive-api.ts"],"sourcesContent":["import type { CloudStorageApi, DriveFile } from \"backless-core\";\n\nconst DRIVE_API = \"https://www.googleapis.com/drive/v3\";\nconst UPLOAD_API = \"https://www.googleapis.com/upload/drive/v3\";\nconst FOLDER_MIME = \"application/vnd.google-apps.folder\";\n\n// Drive API query strings use single-quoted literals; escape embedded single quotes as \\'\nfunction escapeDriveQuery(value: string): string {\n return value.replace(/'/g, \"\\\\'\");\n}\n\nexport class GoogleDriveApi implements CloudStorageApi {\n private readonly folderCache = new Map<string, string>();\n\n constructor(private getToken: () => Promise<string | null>) {}\n\n private async headers(): Promise<HeadersInit> {\n const token = await this.getToken();\n if (!token) throw new Error(\"Not authenticated\");\n return { Authorization: `Bearer ${token}` };\n }\n\n private async request(url: string, init?: RequestInit): Promise<Response> {\n const MAX_RETRIES = 3;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n // headers() is outside the try so auth errors propagate immediately (not retried).\n // It is inside the loop so a fresh token is fetched on each retry attempt.\n const h = await this.headers();\n let resp: Response;\n try {\n resp = await fetch(url, { ...init, headers: { ...h, ...init?.headers } });\n } catch (err) {\n if (attempt < MAX_RETRIES) {\n await sleep(1000 * 2 ** attempt);\n continue;\n }\n throw err;\n }\n\n if (resp.ok) return resp;\n\n const retryable =\n resp.status === 429 ||\n resp.status === 500 ||\n resp.status === 502 ||\n resp.status === 503;\n\n if (retryable && attempt < MAX_RETRIES) {\n let delayMs = 1000 * 2 ** attempt;\n if (resp.status === 429) {\n const ra = resp.headers.get(\"Retry-After\");\n if (ra && /^\\d+$/.test(ra)) delayMs = parseInt(ra, 10) * 1000;\n }\n await sleep(delayMs);\n continue;\n }\n\n const body = await resp.text();\n throw new Error(`Drive API ${resp.status}: ${body}`);\n }\n\n // Unreachable — loop always returns or throws within MAX_RETRIES iterations\n throw new Error(\"Drive API: retry loop exhausted\");\n }\n\n async getOrCreateAppFolder(name: string): Promise<string> {\n return this.ensureFolder(\"root\", name);\n }\n\n async ensureFolder(parentId: string, name: string): Promise<string> {\n const cacheKey = `${parentId}:${name}`;\n const cached = this.folderCache.get(cacheKey);\n if (cached) return cached;\n\n const q = `'${parentId}' in parents and name = '${escapeDriveQuery(name)}' and mimeType = '${FOLDER_MIME}' and trashed = false`;\n const resp = await this.request(\n `${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=files(id,name)&spaces=drive`\n );\n const data = await resp.json();\n\n let folderId: string;\n if (data.files?.length > 0) {\n folderId = data.files[0].id;\n } else {\n const createResp = await this.request(`${DRIVE_API}/files`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n name,\n mimeType: FOLDER_MIME,\n parents: [parentId],\n }),\n });\n const created = await createResp.json();\n folderId = created.id;\n }\n\n this.folderCache.set(cacheKey, folderId);\n return folderId;\n }\n\n async listFiles(folderId: string): Promise<DriveFile[]> {\n const q = `'${folderId}' in parents and trashed = false`;\n const fields = \"files(id,name,mimeType,modifiedTime)\";\n let files: DriveFile[] = [];\n let pageToken: string | undefined;\n\n do {\n let url = `${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=nextPageToken,${fields}&spaces=drive&orderBy=name`;\n if (pageToken) url += `&pageToken=${pageToken}`;\n\n const resp = await this.request(url);\n const data = await resp.json();\n files = files.concat(data.files ?? []);\n pageToken = data.nextPageToken;\n } while (pageToken);\n\n return files;\n }\n\n /**\n * Batched form of {@link listFiles}: one query per chunk of folders instead of\n * one per folder. Drive has no documented cap on OR'd `in parents` terms, but\n * the `q` parameter is URL-length bound — 25 folders per query is comfortably\n * inside it. `parents` is requested so results can be grouped back per folder.\n */\n async listFilesInFolders(folderIds: string[]): Promise<Map<string, DriveFile[]>> {\n const CHUNK = 25;\n const result = new Map<string, DriveFile[]>();\n for (const id of folderIds) result.set(id, []);\n\n for (let i = 0; i < folderIds.length; i += CHUNK) {\n const chunk = folderIds.slice(i, i + CHUNK);\n const parentsClause = chunk.map((id) => `'${id}' in parents`).join(\" or \");\n const q = `(${parentsClause}) and trashed = false`;\n const fields = \"files(id,name,mimeType,modifiedTime,parents)\";\n\n let pageToken: string | undefined;\n do {\n let url = `${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=nextPageToken,${fields}&spaces=drive&orderBy=name&pageSize=1000`;\n if (pageToken) url += `&pageToken=${pageToken}`;\n\n const resp = await this.request(url);\n const data = await resp.json();\n\n for (const file of data.files ?? []) {\n // A file can technically have several parents; attribute it to whichever\n // of the requested folders it belongs to.\n for (const parent of file.parents ?? []) {\n const bucket = result.get(parent);\n if (bucket) bucket.push({ id: file.id, name: file.name, mimeType: file.mimeType, modifiedTime: file.modifiedTime });\n }\n }\n pageToken = data.nextPageToken;\n } while (pageToken);\n }\n\n return result;\n }\n\n async listFolders(parentId: string): Promise<DriveFile[]> {\n const q = `'${parentId}' in parents and mimeType = '${FOLDER_MIME}' and trashed = false`;\n const fields = \"files(id,name,mimeType,modifiedTime)\";\n let files: DriveFile[] = [];\n let pageToken: string | undefined;\n\n do {\n let url = `${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=nextPageToken,${fields}&spaces=drive`;\n if (pageToken) url += `&pageToken=${pageToken}`;\n const resp = await this.request(url);\n const data = await resp.json();\n files = files.concat(data.files ?? []);\n pageToken = data.nextPageToken;\n } while (pageToken);\n\n return files;\n }\n\n async uploadJsonFile(folderId: string, name: string, data: unknown): Promise<string> {\n const metadata = {\n name,\n parents: [folderId],\n mimeType: \"application/json\",\n };\n\n const boundary = \"backless_boundary\";\n const body = [\n `--${boundary}`,\n \"Content-Type: application/json; charset=UTF-8\",\n \"\",\n JSON.stringify(metadata),\n `--${boundary}`,\n \"Content-Type: application/json\",\n \"\",\n JSON.stringify(data),\n `--${boundary}--`,\n ].join(\"\\r\\n\");\n\n const resp = await this.request(\n `${UPLOAD_API}/files?uploadType=multipart&fields=id`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": `multipart/related; boundary=${boundary}`,\n },\n body,\n }\n );\n const result = await resp.json();\n return result.id;\n }\n\n async downloadJsonFile<T = unknown>(fileId: string): Promise<T> {\n const resp = await this.request(`${DRIVE_API}/files/${fileId}?alt=media`);\n return resp.json();\n }\n\n async uploadBinaryFile(folderId: string, name: string, mimeType: string, data: Blob): Promise<string> {\n const metadata = JSON.stringify({ name, parents: [folderId], mimeType });\n const metadataBlob = new Blob([metadata], { type: \"application/json; charset=UTF-8\" });\n const boundary = \"backless_boundary\";\n const body = new Blob([\n `--${boundary}\\r\\nContent-Type: application/json; charset=UTF-8\\r\\n\\r\\n`,\n metadataBlob,\n `\\r\\n--${boundary}\\r\\nContent-Type: ${mimeType}\\r\\n\\r\\n`,\n data,\n `\\r\\n--${boundary}--`,\n ]);\n\n const token = await this.getToken();\n if (!token) throw new Error(\"Not authenticated\");\n const resp = await fetch(`${UPLOAD_API}/files?uploadType=multipart&fields=id`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": `multipart/related; boundary=${boundary}`,\n },\n body,\n });\n if (!resp.ok) {\n const errBody = await resp.text();\n throw new Error(`Drive API ${resp.status}: ${errBody}`);\n }\n const result = await resp.json();\n return result.id;\n }\n\n async downloadBinaryFile(fileId: string): Promise<Blob> {\n const resp = await this.request(`${DRIVE_API}/files/${fileId}?alt=media`);\n return resp.blob();\n }\n\n async deleteFile(fileId: string): Promise<void> {\n await this.request(`${DRIVE_API}/files/${fileId}`, { method: \"DELETE\" });\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/google-drive-api.ts"],"sourcesContent":["import type { CloudStorageApi, DriveFile } from \"backless-core\";\n\nconst DRIVE_API = \"https://www.googleapis.com/drive/v3\";\nconst UPLOAD_API = \"https://www.googleapis.com/upload/drive/v3\";\nconst FOLDER_MIME = \"application/vnd.google-apps.folder\";\n\n// Drive API query strings use single-quoted literals; escape embedded single quotes as \\'\nfunction escapeDriveQuery(value: string): string {\n return value.replace(/'/g, \"\\\\'\");\n}\n\nexport class GoogleDriveApi implements CloudStorageApi {\n private readonly folderCache = new Map<string, string>();\n\n constructor(private getToken: () => Promise<string | null>) {}\n\n private async headers(): Promise<HeadersInit> {\n const token = await this.getToken();\n if (!token) throw new Error(\"Not authenticated\");\n return { Authorization: `Bearer ${token}` };\n }\n\n private async request(url: string, init?: RequestInit): Promise<Response> {\n const MAX_RETRIES = 3;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n // headers() is outside the try so auth errors propagate immediately (not retried).\n // It is inside the loop so a fresh token is fetched on each retry attempt.\n const h = await this.headers();\n let resp: Response;\n try {\n resp = await fetch(url, { ...init, headers: { ...h, ...init?.headers } });\n } catch (err) {\n if (attempt < MAX_RETRIES) {\n await sleep(1000 * 2 ** attempt);\n continue;\n }\n throw err;\n }\n\n if (resp.ok) return resp;\n\n const retryable =\n resp.status === 429 ||\n resp.status === 500 ||\n resp.status === 502 ||\n resp.status === 503;\n\n if (retryable && attempt < MAX_RETRIES) {\n let delayMs = 1000 * 2 ** attempt;\n if (resp.status === 429) {\n const ra = resp.headers.get(\"Retry-After\");\n if (ra && /^\\d+$/.test(ra)) delayMs = parseInt(ra, 10) * 1000;\n }\n await sleep(delayMs);\n continue;\n }\n\n const body = await resp.text();\n throw new Error(`Drive API ${resp.status}: ${body}`);\n }\n\n // Unreachable — loop always returns or throws within MAX_RETRIES iterations\n throw new Error(\"Drive API: retry loop exhausted\");\n }\n\n async getOrCreateAppFolder(name: string): Promise<string> {\n return this.ensureFolder(\"root\", name);\n }\n\n async ensureFolder(parentId: string, name: string): Promise<string> {\n const cacheKey = `${parentId}:${name}`;\n const cached = this.folderCache.get(cacheKey);\n if (cached) return cached;\n\n let folderId = this.oldestFolderId(await this.matchingFolders(parentId, name));\n if (!folderId) {\n // No folder yet — create one, then re-resolve. check-then-create has no locking and\n // Drive allows same-named folders, so a concurrent client may have created its own\n // duplicate in the same window. Re-querying and picking the oldest lets every racer\n // converge on the same folder instead of each keeping the one it made — which split\n // blobs across two `media` folders (F2). Fall back to the created id if the re-query lags.\n const created = await this.createFolder(parentId, name);\n folderId = this.oldestFolderId(await this.matchingFolders(parentId, name)) ?? created;\n }\n\n this.folderCache.set(cacheKey, folderId);\n return folderId;\n }\n\n private async matchingFolders(\n parentId: string,\n name: string,\n ): Promise<{ id: string; createdTime?: string }[]> {\n const q = `'${parentId}' in parents and name = '${escapeDriveQuery(name)}' and mimeType = '${FOLDER_MIME}' and trashed = false`;\n const resp = await this.request(\n `${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=files(id,name,createdTime)&spaces=drive`\n );\n const data = await resp.json();\n return data.files ?? [];\n }\n\n private async createFolder(parentId: string, name: string): Promise<string> {\n const createResp = await this.request(`${DRIVE_API}/files`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ name, mimeType: FOLDER_MIME, parents: [parentId] }),\n });\n const created = await createResp.json();\n return created.id;\n }\n\n /**\n * Deterministic across clients: oldest by createdTime, id as a stable tiebreak.\n * A missing/blank createdTime sorts last (treated as newest) so a folder with a real\n * timestamp always wins over one whose creation time the API happened to omit.\n */\n private oldestFolderId(files: { id: string; createdTime?: string }[]): string | undefined {\n if (files.length === 0) return undefined;\n return [...files].sort((a, b) => {\n const at = a.createdTime;\n const bt = b.createdTime;\n // Missing/blank timestamps sort last (treated as newest).\n if (at !== bt) {\n if (!at) return 1;\n if (!bt) return -1;\n return at < bt ? -1 : 1;\n }\n return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;\n })[0].id;\n }\n\n async listFiles(folderId: string): Promise<DriveFile[]> {\n const q = `'${folderId}' in parents and trashed = false`;\n const fields = \"files(id,name,mimeType,modifiedTime)\";\n let files: DriveFile[] = [];\n let pageToken: string | undefined;\n\n do {\n let url = `${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=nextPageToken,${fields}&spaces=drive&orderBy=name`;\n if (pageToken) url += `&pageToken=${pageToken}`;\n\n const resp = await this.request(url);\n const data = await resp.json();\n files = files.concat(data.files ?? []);\n pageToken = data.nextPageToken;\n } while (pageToken);\n\n return files;\n }\n\n /**\n * Batched form of {@link listFiles}: one query per chunk of folders instead of\n * one per folder. Drive has no documented cap on OR'd `in parents` terms, but\n * the `q` parameter is URL-length bound — 25 folders per query is comfortably\n * inside it. `parents` is requested so results can be grouped back per folder.\n */\n async listFilesInFolders(folderIds: string[]): Promise<Map<string, DriveFile[]>> {\n const CHUNK = 25;\n const result = new Map<string, DriveFile[]>();\n for (const id of folderIds) result.set(id, []);\n\n for (let i = 0; i < folderIds.length; i += CHUNK) {\n const chunk = folderIds.slice(i, i + CHUNK);\n const parentsClause = chunk.map((id) => `'${id}' in parents`).join(\" or \");\n const q = `(${parentsClause}) and trashed = false`;\n const fields = \"files(id,name,mimeType,modifiedTime,parents)\";\n\n let pageToken: string | undefined;\n do {\n let url = `${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=nextPageToken,${fields}&spaces=drive&orderBy=name&pageSize=1000`;\n if (pageToken) url += `&pageToken=${pageToken}`;\n\n const resp = await this.request(url);\n const data = await resp.json();\n\n for (const file of data.files ?? []) {\n // A file can technically have several parents; attribute it to whichever\n // of the requested folders it belongs to.\n for (const parent of file.parents ?? []) {\n const bucket = result.get(parent);\n if (bucket) bucket.push({ id: file.id, name: file.name, mimeType: file.mimeType, modifiedTime: file.modifiedTime });\n }\n }\n pageToken = data.nextPageToken;\n } while (pageToken);\n }\n\n return result;\n }\n\n async listFolders(parentId: string): Promise<DriveFile[]> {\n const q = `'${parentId}' in parents and mimeType = '${FOLDER_MIME}' and trashed = false`;\n const fields = \"files(id,name,mimeType,modifiedTime)\";\n let files: DriveFile[] = [];\n let pageToken: string | undefined;\n\n do {\n let url = `${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=nextPageToken,${fields}&spaces=drive`;\n if (pageToken) url += `&pageToken=${pageToken}`;\n const resp = await this.request(url);\n const data = await resp.json();\n files = files.concat(data.files ?? []);\n pageToken = data.nextPageToken;\n } while (pageToken);\n\n return files;\n }\n\n async uploadJsonFile(folderId: string, name: string, data: unknown): Promise<string> {\n const metadata = {\n name,\n parents: [folderId],\n mimeType: \"application/json\",\n };\n\n const boundary = \"backless_boundary\";\n const body = [\n `--${boundary}`,\n \"Content-Type: application/json; charset=UTF-8\",\n \"\",\n JSON.stringify(metadata),\n `--${boundary}`,\n \"Content-Type: application/json\",\n \"\",\n JSON.stringify(data),\n `--${boundary}--`,\n ].join(\"\\r\\n\");\n\n const resp = await this.request(\n `${UPLOAD_API}/files?uploadType=multipart&fields=id`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": `multipart/related; boundary=${boundary}`,\n },\n body,\n }\n );\n const result = await resp.json();\n return result.id;\n }\n\n async downloadJsonFile<T = unknown>(fileId: string): Promise<T> {\n const resp = await this.request(`${DRIVE_API}/files/${fileId}?alt=media`);\n return resp.json();\n }\n\n async uploadBinaryFile(folderId: string, name: string, mimeType: string, data: Blob): Promise<string> {\n const metadata = JSON.stringify({ name, parents: [folderId], mimeType });\n const metadataBlob = new Blob([metadata], { type: \"application/json; charset=UTF-8\" });\n const boundary = \"backless_boundary\";\n const body = new Blob([\n `--${boundary}\\r\\nContent-Type: application/json; charset=UTF-8\\r\\n\\r\\n`,\n metadataBlob,\n `\\r\\n--${boundary}\\r\\nContent-Type: ${mimeType}\\r\\n\\r\\n`,\n data,\n `\\r\\n--${boundary}--`,\n ]);\n\n const token = await this.getToken();\n if (!token) throw new Error(\"Not authenticated\");\n const resp = await fetch(`${UPLOAD_API}/files?uploadType=multipart&fields=id`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": `multipart/related; boundary=${boundary}`,\n },\n body,\n });\n if (!resp.ok) {\n const errBody = await resp.text();\n throw new Error(`Drive API ${resp.status}: ${errBody}`);\n }\n const result = await resp.json();\n return result.id;\n }\n\n async downloadBinaryFile(fileId: string): Promise<Blob> {\n const resp = await this.request(`${DRIVE_API}/files/${fileId}?alt=media`);\n return resp.blob();\n }\n\n async deleteFile(fileId: string): Promise<void> {\n await this.request(`${DRIVE_API}/files/${fileId}`, { method: \"DELETE\" });\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "url": "https://github.com/mikocot/backless/issues"
9
9
  },
10
10
  "homepage": "https://github.com/mikocot/backless#readme",
11
- "version": "0.9.6",
11
+ "version": "0.9.7",
12
12
  "type": "module",
13
13
  "main": "dist/index.js",
14
14
  "types": "dist/index.d.ts",
@@ -24,7 +24,7 @@
24
24
  "test:watch": "vitest"
25
25
  },
26
26
  "dependencies": {
27
- "backless-core": "^0.9.6"
27
+ "backless-core": "^0.9.7"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsup": "^8.0.0",