Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions nx2/utils/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,23 @@ export const source: {
* @param arg Path string (`/org/site/folder`) or `{ org, site, path }`
*/
deleteFolder(arg: any): Promise<ApiResponse>;

/**
* Copy a folder recursively. `path` is the source folder; `destination`
* is the target folder path. `collision` sets conflict policy when the
* destination exists (e.g. `'overwrite'`). Returns an augmented `Response`.
*
* **hlx6** normalizes `path`/`destination` to a trailing slash before
* dispatch. **Legacy DA** uses them as-is (no trailing slash needed,
* as handled by da-admin).
*
* - **Object:** `copyFolder({ org, site, path, destination, collision? })`
* - **Path:** `copyFolder('/org/site/folder', { destination, collision? })`
*
* @param arg Path string (`/org/site/folder`) or `{ org, site, path, destination, collision? }`
* @param pathExtras Path-form only — `{ destination, collision? }`
*/
copyFolder(arg: any, pathExtras?: object): Promise<ApiResponse>;
};

// ─── versions ───────────────────────────────────────────────────────────────
Expand Down
20 changes: 20 additions & 0 deletions nx2/utils/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,26 @@ export const source = {
const url = await getDaApiPath(SOURCE, org, site, `${path}/`);
return daFetch({ url, opts: { method: 'DELETE' } });
}),

copyFolder: withArgs(async ({
org, site, path, destination, collision,
}) => {
const hlx6 = await isHlx6(org, site);
if (hlx6) {
const folderPath = path.endsWith('/') ? path : `${path}/`;
const folderDestination = destination.endsWith('/') ? destination : `${destination}/`;
const url = new URL(await getDaApiPath(SOURCE, org, site, folderDestination));
url.searchParams.set('source', folderPath);
if (collision) url.searchParams.set('collision', collision);
return daFetch({ url: url.toString(), opts: { method: 'PUT' } });
}
const formData = new FormData();
formData.append('destination', destination);
return daFetch({
url: `${DA_ADMIN}/copy/${org}/${site}${path}`,
opts: { method: 'POST', body: formData },
});
}),
};

// status: single-path only. H6 has no bulk status endpoint.
Expand Down
12 changes: 12 additions & 0 deletions nx2/utils/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Document CRUD on `source` paths. Bridges DA's `/source` and AEM's `/sites/{site}
| `move` | `({ org, site, path, destination, collision? })` or `(fullPath, { destination, collision? })` | Same shape as `copy`. Raw `Response`. Adds `?move=true` (hlx6) or POSTs to `/move/{org}/{site}{path}` (DA). |
| `createFolder` | `({ org, site, path })` or `(fullPath)` | POST on `${path}/` (trailing slash). |
| `deleteFolder` | `({ org, site, path })` or `(fullPath)` | DELETE on `${path}/`. |
| `copyFolder` | `({ org, site, path, destination, collision? })` or `(fullPath, { destination, collision? })` | Recursive folder copy. Same shape as `copy`. **hlx6**: `path`/`destination` are normalized to a trailing slash first, then PUT to dest URL with `?source=…/&collision=…` query. **DA**: POST `/copy/{org}/{site}{path}` with `multipart/form-data` field `destination` — no trailing-slash normalization (legacy DA doesn't need it). |


### URL shapes
Expand All @@ -134,6 +135,7 @@ Document CRUD on `source` paths. Bridges DA's `/source` and AEM's `/sites/{site}
| list (org-only) | n/a | `${DA_ADMIN}/list/{org}` |
| list (with site, legacy) | n/a | `${DA_ADMIN}/list/{org}/{site}{path}` |
| copy / move | PUT to dest URL with `?source=&collision=&move=` | POST to `${DA_ADMIN}/copy/{org}/{site}{path}` (or `/move`) with `destination` form field |
| copyFolder | PUT to dest URL (trailing slash normalized) with `?source=&collision=` | POST to `${DA_ADMIN}/copy/{org}/{site}{path}` with `destination` form field (no trailing-slash normalization) |


### Examples
Expand Down Expand Up @@ -167,6 +169,16 @@ const copyResp = await source.copy({
destination: '/new.html', // dest
collision: 'overwrite',
});

// Copy a folder recursively — path/destination don't need a trailing slash.
// On hlx6 they're normalized to one before dispatch; legacy DA uses them as-is.
const copyFolderResp = await source.copyFolder({
org: 'adobe',
site: 'aem-boilerplate',
path: '/old-folder', // source folder
destination: '/new-folder', // dest folder
collision: 'overwrite',
});
```

---
Expand Down
39 changes: 39 additions & 0 deletions test/nx2/utils/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,45 @@ describe('api.js', () => {
expect(last.url).to.equal(`${AEM_API}/${o}/sites/${s}/source/folder/`);
expect(last.method).to.equal('DELETE');
});

it('source.copyFolder hlx6 PUTs with trailing-slash source/destination and collision query', async () => {
const { org: o, site: s } = makeOrgSite({ hlx6: true });
await source.copyFolder({
org: o, site: s, path: '/src', destination: '/dest', collision: 'overwrite',
});
const last = lastCall();
expect(last.method).to.equal('PUT');
const u = new URL(last.url);
expect(u.pathname).to.equal(`/${o}/sites/${s}/source/dest/`);
expect(u.searchParams.get('source')).to.equal('/src/');
expect(u.searchParams.get('collision')).to.equal('overwrite');
});

it('source.copyFolder legacy POSTs to /copy/{org}/{site}{path} with destination form field (no trailing slash)', async () => {
const { org: o, site: s } = makeOrgSite();
await source.copyFolder({ org: o, site: s, path: '/src', destination: '/dest' });
const last = lastCall();
expect(last.url).to.equal(`${DA_ADMIN}/copy/${o}/${s}/src`);
expect(last.method).to.equal('POST');
expect(last.body).to.be.instanceof(FormData);
expect(last.body.get('destination')).to.equal('/dest');
});

it('source.copyFolder normalizes paths that already have a trailing slash', async () => {
const { org: o, site: s } = makeOrgSite({ hlx6: true });
await source.copyFolder({ org: o, site: s, path: '/src/', destination: '/dest/' });
const u = new URL(lastCall().url);
expect(u.pathname).to.equal(`/${o}/sites/${s}/source/dest/`);
expect(u.searchParams.get('source')).to.equal('/src/');
});

it('source.copyFolder accepts a path string with extras', async () => {
const { org: o, site: s } = makeOrgSite({ hlx6: true });
await source.copyFolder(`/${o}/${s}/src`, { destination: '/dest' });
const u = new URL(lastCall().url);
expect(u.pathname).to.equal(`/${o}/sites/${s}/source/dest/`);
expect(u.searchParams.get('source')).to.equal('/src/');
});
});

describe('versions', () => {
Expand Down
Loading