Skip to content
30 changes: 30 additions & 0 deletions .changeset/chatty-areas-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
'@mastra/client-js': minor
'@mastra/server': patch
'@mastra/mongodb': patch
'@mastra/spanner': patch
'@mastra/core': patch
'@mastra/libsql': patch
'@mastra/mysql': patch
'@mastra/pg': patch
---

Added optional tenancy arguments to `getDataset`, `updateDataset`, and `deleteDataset`.

You can now pass `organizationId` and `projectId` to scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throw `DATASET_NOT_FOUND` (surfaced as a 404 over HTTP). Deletes silently no-op on a tenancy mismatch — matching the existing "delete non-existent id is a no-op" semantics so cross-tenant existence is never leaked via error timing or status.

**Example**

```ts
// Before
await client.getDataset('abc123');
await client.deleteDataset('abc123');
await client.updateDataset({ id: 'abc123', name: 'renamed' });

// After — scope to a tenant
await client.getDataset('abc123', { organizationId: 'org_a', projectId: 'proj_1' });
await client.deleteDataset('abc123', { organizationId: 'org_a' });
await client.updateDataset({ id: 'abc123', name: 'renamed', organizationId: 'org_a' });
```

Related: [MASTRA-4438](https://linear.app/kepler-crm/issue/MASTRA-4438)
40 changes: 40 additions & 0 deletions .changeset/pink-mails-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
'@mastra/core': minor
'@mastra/client-js': patch
'@mastra/server': patch
'@mastra/mongodb': patch
'@mastra/spanner': patch
'@mastra/libsql': patch
'@mastra/mysql': patch
'@mastra/pg': patch
---

Fixed a cross-tenant data-access issue on datasets by scoping `DatasetsManager.get` and `DatasetsManager.delete` to tenancy filters.

Previously `get({ id })` and `delete({ id })` looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of which `organizationId` / `projectId` it belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).

**What changed**

- `DatasetsManager.get` and `DatasetsManager.delete` accept optional `organizationId` and `projectId`.
- The tenancy is stashed on the returned `Dataset` handle and forwarded to every downstream storage call (`getDetails`, `update`, `addItem`, item batch ops, `startExperimentAsync`).
- The abstract storage contract (`getDatasetById`, `deleteDataset`) gained an optional `filters?: DatasetTenancyFilters` arg.
- Item-mutation inputs (`AddDatasetItemInput`, `UpdateDatasetItemInput`, `BatchInsertItemsInput`, `BatchDeleteItemsInput`) and `UpdateDatasetInput` accept optional `filters` for the internal existence check.

**Behavior**

- Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
- On tenancy mismatch, `get` throws NOT_FOUND (returns null at the storage layer) and `delete` is a silent no-op — matching how a missing id already behaves, so existence does not leak through error timing or messages.

**Example**

```ts
// Before
const ds = await mastra.datasets.get({ id });
await mastra.datasets.delete({ id });

// After — scope to a tenant
const ds = await mastra.datasets.get({ id, organizationId, projectId });
await mastra.datasets.delete({ id, organizationId, projectId });
```

Related: [MASTRA-4438](https://linear.app/kepler-crm/issue/MASTRA-4438)
16 changes: 16 additions & 0 deletions .changeset/swift-things-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@mastra/client-js': patch
'@mastra/server': patch
'@mastra/mongodb': patch
'@mastra/spanner': patch
'@mastra/core': patch
'@mastra/libsql': patch
'@mastra/mysql': patch
'@mastra/pg': patch
---

Scoped `getDatasetById` and `deleteDataset` to tenancy filters when the caller passes `organizationId` / `projectId`.

The adapters now push the tenancy predicate into the SQL/query when the new optional `filters` argument is present. Legacy calls that omit tenancy are unchanged. On mismatch, `getDatasetById` returns `null` and `deleteDataset` is a silent no-op — the cascade delete (dataset items and versions) is gated by a scoped parent pre-check, so cross-tenant data is never touched.

Related: [MASTRA-4438](https://linear.app/kepler-crm/issue/MASTRA-4438)
23 changes: 23 additions & 0 deletions .changeset/tired-donuts-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@mastra/server': minor
'@mastra/client-js': patch
'@mastra/mongodb': patch
'@mastra/spanner': patch
'@mastra/core': patch
'@mastra/libsql': patch
'@mastra/mysql': patch
'@mastra/pg': patch
---

Added optional `organizationId` and `projectId` query parameters to the dataset routes.

`GET /datasets/:datasetId`, `PATCH /datasets/:datasetId`, and `DELETE /datasets/:datasetId` now accept optional tenancy query parameters. When provided, they are forwarded to `mastra.datasets.get` / `.delete` and the operation returns 404 if the dataset does not belong to the requested tenant. Requests that omit the query parameters keep their existing behavior.

**Example**

```
GET /datasets/abc123?organizationId=org_a&projectId=proj_1
DELETE /datasets/abc123?organizationId=org_a
```

Related: [MASTRA-4438](https://linear.app/kepler-crm/issue/MASTRA-4438)
35 changes: 25 additions & 10 deletions client-sdks/client-js/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ import type {
UpdateHeartbeatOptions,
RunHeartbeatResponse,
} from './types';
import { base64RequestContext, parseClientRequestContext, requestContextQueryString } from './utils';
import { base64RequestContext, buildTenancyQuery, parseClientRequestContext, requestContextQueryString } from './utils';

export class MastraClient extends BaseResource {
private observability: Observability;
Expand Down Expand Up @@ -1786,10 +1786,16 @@ export class MastraClient extends BaseResource {
}

/**
* Gets a single dataset by ID
* Gets a single dataset by ID. Optionally scope the lookup to a specific
* tenant organization/project — the server returns 404 if the dataset does
* not belong to the given tenant.
*/
public getDataset(datasetId: string): Promise<DatasetRecord> {
return this.request(`/datasets/${encodeURIComponent(datasetId)}`);
public getDataset(
datasetId: string,
tenancy?: { organizationId?: string; projectId?: string },
): Promise<DatasetRecord> {
const qs = buildTenancyQuery(tenancy);
return this.request(`/datasets/${encodeURIComponent(datasetId)}${qs}`);
}

/**
Expand All @@ -1800,21 +1806,30 @@ export class MastraClient extends BaseResource {
}

/**
* Updates a dataset
* Updates a dataset. Tenancy fields, when provided, scope the existence
* check on the server side so that a caller can only update datasets that
* belong to the given tenant.
*/
public updateDataset(params: UpdateDatasetParams): Promise<DatasetRecord> {
const { datasetId, ...body } = params;
return this.request(`/datasets/${encodeURIComponent(datasetId)}`, {
const { datasetId, organizationId, projectId, ...body } = params;
const qs = buildTenancyQuery({ organizationId, projectId });
return this.request(`/datasets/${encodeURIComponent(datasetId)}${qs}`, {
method: 'PATCH',
body,
});
}

/**
* Deletes a dataset
* Deletes a dataset. When tenancy fields are supplied, the server only
* deletes the dataset if it belongs to the given tenant (silent no-op
* otherwise).
*/
public deleteDataset(datasetId: string): Promise<{ success: boolean }> {
return this.request(`/datasets/${encodeURIComponent(datasetId)}`, {
public deleteDataset(
datasetId: string,
tenancy?: { organizationId?: string; projectId?: string },
): Promise<{ success: boolean }> {
const qs = buildTenancyQuery(tenancy);
return this.request(`/datasets/${encodeURIComponent(datasetId)}${qs}`, {
method: 'DELETE',
});
}
Expand Down
45 changes: 39 additions & 6 deletions client-sdks/client-js/src/route-types.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85758,6 +85758,13 @@ export type GetDatasetsDatasetId_PathParams = {
datasetId: string;
};

export type GetDatasetsDatasetId_QueryParams = {
/** Restrict lookup to the given organization */
organizationId?: string | undefined;
/** Restrict lookup to the given project */
projectId?: string | undefined;
};

export type GetDatasetsDatasetId_Response = {
id: string;
name: string;
Expand Down Expand Up @@ -85796,13 +85803,17 @@ export type GetDatasetsDatasetId_Response = {

export type GetDatasetsDatasetId_Request = Simplify<
(GetDatasetsDatasetId_PathParams extends never ? {} : { params: GetDatasetsDatasetId_PathParams }) &
(never extends never ? {} : {} extends never ? { query?: never } : { query: never }) &
(GetDatasetsDatasetId_QueryParams extends never
? {}
: {} extends GetDatasetsDatasetId_QueryParams
? { query?: GetDatasetsDatasetId_QueryParams }
: { query: GetDatasetsDatasetId_QueryParams }) &
(never extends never ? {} : {} extends never ? { body?: never } : { body: never })
>;

export interface GetDatasetsDatasetId_RouteContract {
pathParams: GetDatasetsDatasetId_PathParams;
queryParams: never;
queryParams: GetDatasetsDatasetId_QueryParams;
body: never;
request: GetDatasetsDatasetId_Request;
response: GetDatasetsDatasetId_Response;
Expand All @@ -85817,6 +85828,13 @@ export type PatchDatasetsDatasetId_PathParams = {
datasetId: string;
};

export type PatchDatasetsDatasetId_QueryParams = {
/** Restrict lookup to the given organization */
organizationId?: string | undefined;
/** Restrict lookup to the given project */
projectId?: string | undefined;
};

export type PatchDatasetsDatasetId_Body = {
/** Name of the dataset */
name?: string | undefined;
Expand Down Expand Up @@ -85894,7 +85912,11 @@ export type PatchDatasetsDatasetId_Response = {

export type PatchDatasetsDatasetId_Request = Simplify<
(PatchDatasetsDatasetId_PathParams extends never ? {} : { params: PatchDatasetsDatasetId_PathParams }) &
(never extends never ? {} : {} extends never ? { query?: never } : { query: never }) &
(PatchDatasetsDatasetId_QueryParams extends never
? {}
: {} extends PatchDatasetsDatasetId_QueryParams
? { query?: PatchDatasetsDatasetId_QueryParams }
: { query: PatchDatasetsDatasetId_QueryParams }) &
(PatchDatasetsDatasetId_Body extends never
? {}
: {} extends PatchDatasetsDatasetId_Body
Expand All @@ -85904,7 +85926,7 @@ export type PatchDatasetsDatasetId_Request = Simplify<

export interface PatchDatasetsDatasetId_RouteContract {
pathParams: PatchDatasetsDatasetId_PathParams;
queryParams: never;
queryParams: PatchDatasetsDatasetId_QueryParams;
body: PatchDatasetsDatasetId_Body;
request: PatchDatasetsDatasetId_Request;
response: PatchDatasetsDatasetId_Response;
Expand All @@ -85919,19 +85941,30 @@ export type DeleteDatasetsDatasetId_PathParams = {
datasetId: string;
};

export type DeleteDatasetsDatasetId_QueryParams = {
/** Restrict lookup to the given organization */
organizationId?: string | undefined;
/** Restrict lookup to the given project */
projectId?: string | undefined;
};

export type DeleteDatasetsDatasetId_Response = {
success: boolean;
};

export type DeleteDatasetsDatasetId_Request = Simplify<
(DeleteDatasetsDatasetId_PathParams extends never ? {} : { params: DeleteDatasetsDatasetId_PathParams }) &
(never extends never ? {} : {} extends never ? { query?: never } : { query: never }) &
(DeleteDatasetsDatasetId_QueryParams extends never
? {}
: {} extends DeleteDatasetsDatasetId_QueryParams
? { query?: DeleteDatasetsDatasetId_QueryParams }
: { query: DeleteDatasetsDatasetId_QueryParams }) &
(never extends never ? {} : {} extends never ? { body?: never } : { body: never })
>;

export interface DeleteDatasetsDatasetId_RouteContract {
pathParams: DeleteDatasetsDatasetId_PathParams;
queryParams: never;
queryParams: DeleteDatasetsDatasetId_QueryParams;
body: never;
request: DeleteDatasetsDatasetId_Request;
response: DeleteDatasetsDatasetId_Response;
Expand Down
4 changes: 4 additions & 0 deletions client-sdks/client-js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2723,6 +2723,10 @@ export interface UpdateDatasetParams {
targetType?: string;
targetIds?: string[];
scorerIds?: string[] | null;
/** Restrict the lookup to a specific tenant organization. */
organizationId?: string;
/** Restrict the lookup to a specific tenant project. */
projectId?: string;
}

export interface AddDatasetItemParams {
Expand Down
14 changes: 14 additions & 0 deletions client-sdks/client-js/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,17 @@ export function requestContextQueryString(
const queryString = searchParams.toString();
return queryString ? `${delimiter}${queryString}` : '';
}

/**
* Build a `?organizationId=...&projectId=...` query string fragment from
* optional tenancy scope fields. Returns an empty string when neither field
* is supplied so callers can safely concatenate it to a URL.
*/
export function buildTenancyQuery(tenancy?: { organizationId?: string; projectId?: string }): string {
if (!tenancy) return '';
const searchParams = new URLSearchParams();
if (tenancy.organizationId) searchParams.set('organizationId', tenancy.organizationId);
if (tenancy.projectId) searchParams.set('projectId', tenancy.projectId);
const queryString = searchParams.toString();
return queryString ? `?${queryString}` : '';
}
21 changes: 15 additions & 6 deletions packages/cli/src/commands/api/route-metadata.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4794,9 +4794,12 @@ export const API_ROUTE_METADATA = {
"pathParams": [
"datasetId"
],
"queryParams": [],
"queryParams": [
"organizationId",
"projectId"
],
"bodyParams": [],
"hasQuery": false,
"hasQuery": true,
"hasBody": false,
"responseShape": {
"kind": "single"
Expand All @@ -4808,7 +4811,10 @@ export const API_ROUTE_METADATA = {
"pathParams": [
"datasetId"
],
"queryParams": [],
"queryParams": [
"organizationId",
"projectId"
],
"bodyParams": [
"description",
"groundTruthSchema",
Expand All @@ -4821,7 +4827,7 @@ export const API_ROUTE_METADATA = {
"targetIds",
"targetType"
],
"hasQuery": false,
"hasQuery": true,
"hasBody": true,
"responseShape": {
"kind": "single"
Expand All @@ -4833,9 +4839,12 @@ export const API_ROUTE_METADATA = {
"pathParams": [
"datasetId"
],
"queryParams": [],
"queryParams": [
"organizationId",
"projectId"
],
"bodyParams": [],
"hasQuery": false,
"hasQuery": true,
"hasBody": false,
"responseShape": {
"kind": "single"
Expand Down
Loading
Loading