Merge pull request #11331 from appwrite/feat-schedules-endpoints

Add schedules API endpoints
This commit is contained in:
Jake Barnby
2026-02-18 08:40:23 +00:00
committed by GitHub
17 changed files with 910 additions and 7 deletions
+8
View File
@@ -405,6 +405,14 @@ return [
'$description' => 'This event triggers when a provider is deleted.'
],
],
'schedules' => [
'$model' => Response::MODEL_SCHEDULE,
'$resource' => true,
'$description' => 'This event triggers on any schedule event.',
'create' => [
'$description' => 'This event triggers when a schedule is created.',
],
],
'rules' => [
'$model' => Response::MODEL_PROXY_RULE,
'$resource' => true,
+2
View File
@@ -91,6 +91,8 @@ $admins = [
'subscribers.read',
'tokens.read',
'tokens.write',
'schedules.read',
'schedules.write',
];
return [
+6
View File
@@ -145,6 +145,12 @@ return [ // List of publicly visible scopes
'rules.write' => [
'description' => 'Access to create, update, and delete your project\'s proxy rules',
],
'schedules.read' => [
'description' => 'Access to read your project\'s schedules',
],
'schedules.write' => [
'description' => 'Access to create, update, and delete your project\'s schedules',
],
'migrations.read' => [
'description' => 'Access to read your project\'s migrations',
],
+3
View File
@@ -114,6 +114,7 @@ use Appwrite\Utopia\Response\Model\ResourceToken;
use Appwrite\Utopia\Response\Model\Row;
use Appwrite\Utopia\Response\Model\Rule;
use Appwrite\Utopia\Response\Model\Runtime;
use Appwrite\Utopia\Response\Model\Schedule;
use Appwrite\Utopia\Response\Model\Session;
use Appwrite\Utopia\Response\Model\Site;
use Appwrite\Utopia\Response\Model\Specification;
@@ -198,6 +199,7 @@ Response::setModel(new BaseList('Metric List', Response::MODEL_METRIC_LIST, 'met
Response::setModel(new BaseList('Variables List', Response::MODEL_VARIABLE_LIST, 'variables', Response::MODEL_VARIABLE));
Response::setModel(new BaseList('Status List', Response::MODEL_HEALTH_STATUS_LIST, 'statuses', Response::MODEL_HEALTH_STATUS));
Response::setModel(new BaseList('Rule List', Response::MODEL_PROXY_RULE_LIST, 'rules', Response::MODEL_PROXY_RULE));
Response::setModel(new BaseList('Schedules List', Response::MODEL_SCHEDULE_LIST, 'schedules', Response::MODEL_SCHEDULE));
Response::setModel(new BaseList('Locale codes list', Response::MODEL_LOCALE_CODE_LIST, 'localeCodes', Response::MODEL_LOCALE_CODE));
Response::setModel(new BaseList('Provider list', Response::MODEL_PROVIDER_LIST, 'providers', Response::MODEL_PROVIDER));
Response::setModel(new BaseList('Message list', Response::MODEL_MESSAGE_LIST, 'messages', Response::MODEL_MESSAGE));
@@ -339,6 +341,7 @@ Response::setModel(new UsageProject());
Response::setModel(new Headers());
Response::setModel(new Specification());
Response::setModel(new Rule());
Response::setModel(new Schedule());
Response::setModel(new TemplateSMS());
Response::setModel(new TemplateEmail());
Response::setModel(new ConsoleVariables());
Generated
+7 -7
View File
@@ -3797,16 +3797,16 @@
},
{
"name": "utopia-php/database",
"version": "5.3.0",
"version": "5.3.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "5e49f32ebb6e40a78209231564c40e5068810429"
"reference": "e3305f6ca64eb5dd715e30212fd32d5d834a3a46"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/5e49f32ebb6e40a78209231564c40e5068810429",
"reference": "5e49f32ebb6e40a78209231564c40e5068810429",
"url": "https://api.github.com/repos/utopia-php/database/zipball/e3305f6ca64eb5dd715e30212fd32d5d834a3a46",
"reference": "e3305f6ca64eb5dd715e30212fd32d5d834a3a46",
"shasum": ""
},
"require": {
@@ -3849,9 +3849,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.3.0"
"source": "https://github.com/utopia-php/database/tree/5.3.1"
},
"time": "2026-02-17T11:50:01+00:00"
"time": "2026-02-18T07:15:40+00:00"
},
{
"name": "utopia-php/detector",
@@ -8885,7 +8885,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -0,0 +1 @@
Create a new schedule for a resource.
+1
View File
@@ -0,0 +1 @@
Get a schedule by its unique ID.
@@ -0,0 +1 @@
Get a list of all the project's schedules. You can use the query params to filter your results.
@@ -0,0 +1,154 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Http\Schedules;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Task\Validator\Cron;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\JSON;
use Utopia\Validator\WhiteList;
class Create extends Action
{
use HTTP;
public static function getName(): string
{
return 'createSchedule';
}
protected function getResourceTypes(): array
{
return [
SCHEDULE_RESOURCE_TYPE_FUNCTION,
SCHEDULE_RESOURCE_TYPE_EXECUTION,
SCHEDULE_RESOURCE_TYPE_MESSAGE,
];
}
protected function getCollection(string $resourceType): string
{
return match ($resourceType) {
SCHEDULE_RESOURCE_TYPE_FUNCTION => 'functions',
SCHEDULE_RESOURCE_TYPE_EXECUTION => 'executions',
SCHEDULE_RESOURCE_TYPE_MESSAGE => 'messages',
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Invalid resource type: ' . $resourceType),
};
}
protected function getNotFoundException(string $resourceType): string
{
return match ($resourceType) {
SCHEDULE_RESOURCE_TYPE_FUNCTION => Exception::FUNCTION_NOT_FOUND,
SCHEDULE_RESOURCE_TYPE_EXECUTION => Exception::EXECUTION_NOT_FOUND,
SCHEDULE_RESOURCE_TYPE_MESSAGE => Exception::MESSAGE_NOT_FOUND,
default => Exception::GENERAL_ARGUMENT_INVALID,
};
}
public function __construct()
{
$resourceTypes = $this->getResourceTypes();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/projects/:projectId/schedules')
->desc('Create schedule')
->groups(['api', 'projects'])
->label('scope', 'schedules.write')
->label('event', 'schedules.[scheduleId].create')
->label('audits.event', 'schedule.create')
->label('audits.resource', 'schedule/{response.$id}')
->label('sdk', new Method(
namespace: 'projects',
group: 'schedules',
name: 'createSchedule',
description: '/docs/references/projects/create-schedule.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
model: Response::MODEL_SCHEDULE,
),
],
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('resourceType', '', new WhiteList($resourceTypes, true), 'The resource type for the schedule. Possible values: '.implode(', ', $resourceTypes).'.')
->param('resourceId', '', new UID(), 'The resource ID to associate with this schedule.')
->param('schedule', '', new Cron(), 'Schedule CRON expression.')
->param('active', false, new Boolean(), 'Whether the schedule is active.', true)
->param('data', null, new JSON(), 'Schedule data as a JSON string. Used to store resource-specific context needed for execution.', true)
->inject('response')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(
string $projectId,
string $resourceType,
string $resourceId,
string $schedule,
bool $active,
?string $data,
Response $response,
Database $dbForPlatform,
callable $getProjectDB,
Event $queueForEvents,
): void {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$dbForProject = $getProjectDB($project);
$collection = $this->getCollection($resourceType);
$resource = $dbForProject->getDocument($collection, $resourceId);
if ($resource->isEmpty()) {
throw new Exception($this->getNotFoundException($resourceType), 'Resource not found');
}
$attributes = [
'region' => $project->getAttribute('region'),
'resourceType' => $resourceType,
'resourceId' => $resourceId,
'resourceInternalId' => $resource->getSequence(),
'resourceUpdatedAt' => DateTime::now(),
'projectId' => $project->getId(),
'schedule' => $schedule,
'active' => $active,
];
if ($data !== null) {
$attributes['data'] = \json_decode($data, true);
}
try {
$doc = $dbForPlatform->createDocument('schedules', new Document($attributes));
} catch (DuplicateException) {
throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS);
}
$queueForEvents->setParam('scheduleId', $doc->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($doc, Response::MODEL_SCHEDULE);
}
}
@@ -0,0 +1,76 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Http\Schedules;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getSchedule';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/projects/:projectId/schedules/:scheduleId')
->desc('Get schedule')
->groups(['api', 'projects'])
->label('scope', 'schedules.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'schedules',
name: 'getSchedule',
description: '/docs/references/projects/get-schedule.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SCHEDULE,
)
],
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('scheduleId', '', new UID(), 'Schedule ID.')
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(
string $projectId,
string $scheduleId,
Response $response,
Database $dbForPlatform,
): void {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$schedule = $dbForPlatform->getDocument('schedules', $scheduleId);
if ($schedule->isEmpty()) {
throw new Exception(Exception::SCHEDULE_NOT_FOUND);
}
if ($schedule->getAttribute('projectId') !== $project->getId()) {
throw new Exception(Exception::SCHEDULE_NOT_FOUND);
}
$response->dynamic($schedule, Response::MODEL_SCHEDULE);
}
}
@@ -0,0 +1,114 @@
<?php
namespace Appwrite\Platform\Modules\Projects\Http\Schedules;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Schedules;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Action
{
use HTTP;
public static function getName(): string
{
return 'listSchedules';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/projects/:projectId/schedules')
->desc('List schedules')
->groups(['api', 'projects'])
->label('scope', 'schedules.read')
->label('sdk', new Method(
namespace: 'projects',
group: 'schedules',
name: 'listSchedules',
description: '/docs/references/projects/list-schedules.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_SCHEDULE_LIST,
)
],
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('queries', [], new Schedules(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Schedules::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForPlatform')
->callback($this->action(...));
}
public function action(
string $projectId,
array $queries,
bool $includeTotal,
Response $response,
Database $dbForPlatform,
): void {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries[] = Query::equal('projectId', [$project->getId()]);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$scheduleId = $cursor->getValue();
$cursorDocument = $dbForPlatform->getDocument('schedules', $scheduleId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Schedule '{$scheduleId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$schedules = $dbForPlatform->find('schedules', $queries);
$total = $includeTotal ? $dbForPlatform->count('schedules', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$response->dynamic(new Document([
'schedules' => $schedules,
'total' => $total,
]), Response::MODEL_SCHEDULE_LIST);
}
}
@@ -12,6 +12,9 @@ use Appwrite\Platform\Modules\Projects\Http\Projects\Labels\Update as UpdateProj
use Appwrite\Platform\Modules\Projects\Http\Projects\Team\Update as UpdateProjectTeam;
use Appwrite\Platform\Modules\Projects\Http\Projects\Update as UpdateProject;
use Appwrite\Platform\Modules\Projects\Http\Projects\XList as ListProjects;
use Appwrite\Platform\Modules\Projects\Http\Schedules\Create as CreateSchedule;
use Appwrite\Platform\Modules\Projects\Http\Schedules\Get as GetSchedule;
use Appwrite\Platform\Modules\Projects\Http\Schedules\XList as ListSchedules;
use Utopia\Platform\Service;
class Http extends Service
@@ -30,5 +33,9 @@ class Http extends Service
$this->addAction(ListProjects::getName(), new ListProjects());
$this->addAction(UpdateProjectLabels::getName(), new UpdateProjectLabels());
$this->addAction(UpdateProjectTeam::getName(), new UpdateProjectTeam());
$this->addAction(CreateSchedule::getName(), new CreateSchedule());
$this->addAction(GetSchedule::getName(), new GetSchedule());
$this->addAction(ListSchedules::getName(), new ListSchedules());
}
}
@@ -0,0 +1,24 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Schedules extends Base
{
public const ALLOWED_ATTRIBUTES = [
'resourceType',
'resourceId',
'projectId',
'schedule',
'active',
'region',
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('schedules', self::ALLOWED_ATTRIBUTES);
}
}
+4
View File
@@ -223,6 +223,10 @@ class Response extends SwooleResponse
public const MODEL_PROXY_RULE = 'proxyRule';
public const MODEL_PROXY_RULE_LIST = 'proxyRuleList';
// Schedules
public const MODEL_SCHEDULE = 'schedule';
public const MODEL_SCHEDULE_LIST = 'scheduleList';
// Migrations
public const MODEL_MIGRATION = 'migration';
public const MODEL_MIGRATION_LIST = 'migrationList';
@@ -0,0 +1,101 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Schedule extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Schedule ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Schedule creation date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Schedule update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('resourceType', [
'type' => self::TYPE_STRING,
'description' => 'The resource type associated with this schedule.',
'default' => '',
'example' => 'function',
])
->addRule('resourceId', [
'type' => self::TYPE_STRING,
'description' => 'The resource ID associated with this schedule.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('resourceUpdatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Change-tracking timestamp used by the scheduler to detect resource changes in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('projectId', [
'type' => self::TYPE_STRING,
'description' => 'The project ID associated with this schedule.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('schedule', [
'type' => self::TYPE_STRING,
'description' => 'The CRON schedule expression.',
'default' => '',
'example' => '5 4 * * *',
])
->addRule('data', [
'type' => self::TYPE_JSON,
'description' => 'Schedule data used to store resource-specific context needed for execution.',
'default' => [],
'example' => [],
])
->addRule('active', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Whether the schedule is active.',
'default' => false,
'example' => true,
])
->addRule('region', [
'type' => self::TYPE_STRING,
'description' => 'The region where the schedule is deployed.',
'default' => '',
'example' => 'fra',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Schedule';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_SCHEDULE;
}
}
@@ -0,0 +1,60 @@
<?php
namespace Tests\E2E\Services\Projects\Schedules;
use Tests\E2E\Client;
use Utopia\Database\Helpers\ID;
use Utopia\System\System;
trait SchedulesBase
{
public function testCreateProject(): array
{
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => ID::unique(),
'name' => 'Schedule Test Team',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$project = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Schedule Test Project',
'teamId' => $team['body']['$id'],
'region' => System::getEnv('_APP_REGION', 'default'),
]);
$this->assertEquals(201, $project['headers']['status-code']);
$projectId = $project['body']['$id'];
$key = $this->client->call(Client::METHOD_POST, '/projects/'.$projectId.'/keys', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'keyId' => ID::unique(),
'name' => 'Schedule Test Key',
'scopes' => [
'functions.read',
'functions.write',
'execution.read',
'execution.write',
'messages.read',
'messages.write',
],
]);
$this->assertEquals(201, $key['headers']['status-code']);
return [
'projectId' => $projectId,
'apiKey' => $key['body']['secret'],
];
}
}
@@ -0,0 +1,341 @@
<?php
namespace Tests\E2E\Services\Projects\Schedules;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectConsole;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\System\System;
class SchedulesConsoleClientTest extends Scope
{
use ProjectConsole;
use SchedulesBase;
use SideClient;
/**
* @depends testCreateProject
*/
public function testCreateSchedule($data): array
{
$id = $data['projectId'] ?? '';
$apiKey = $data['apiKey'] ?? '';
/**
* Test for SUCCESS
*/
$function = $this->client->call(Client::METHOD_POST, '/functions', [
'content-type' => 'application/json',
'x-appwrite-project' => $id,
'x-appwrite-key' => $apiKey,
], [
'functionId' => ID::unique(),
'name' => 'Test Schedule Function',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'execute' => ['any'],
]);
$this->assertEquals(201, $function['headers']['status-code']);
$functionId = $function['body']['$id'];
$response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'resourceType' => 'function',
'resourceId' => $functionId,
'schedule' => '0 0 * * *',
'active' => true,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['$createdAt']);
$this->assertNotEmpty($response['body']['$updatedAt']);
$this->assertEquals('function', $response['body']['resourceType']);
$this->assertEquals($functionId, $response['body']['resourceId']);
$this->assertNotEmpty($response['body']['resourceUpdatedAt']);
$this->assertNotEmpty($response['body']['projectId']);
$this->assertEquals('0 0 * * *', $response['body']['schedule']);
$this->assertArrayHasKey('data', $response['body']);
$this->assertTrue($response['body']['active']);
$this->assertNotEmpty($response['body']['region']);
// Create with data
$scheduleData = ['key' => 'value'];
$responseWithData = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'resourceType' => 'function',
'resourceId' => $functionId,
'schedule' => '0 12 * * *',
'active' => true,
'data' => json_encode($scheduleData),
]);
$this->assertEquals(201, $responseWithData['headers']['status-code']);
$this->assertEquals($scheduleData, $responseWithData['body']['data']);
$data = array_merge($data, [
'scheduleId' => $response['body']['$id'],
'functionId' => $functionId,
]);
/**
* Test for FAILURE
*/
// Resource not found
$response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'resourceType' => 'function',
'resourceId' => ID::unique(),
'schedule' => '0 0 * * *',
]);
$this->assertEquals(404, $response['headers']['status-code']);
// Invalid resource type
$response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'resourceType' => 'invalid',
'resourceId' => ID::unique(),
'schedule' => '0 0 * * *',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Invalid cron
$response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'resourceType' => 'function',
'resourceId' => ID::unique(),
'schedule' => 'not-a-cron',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Missing resourceType
$response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'resourceId' => ID::unique(),
'schedule' => '0 0 * * *',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Missing resourceId
$response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'resourceType' => 'function',
'schedule' => '0 0 * * *',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Missing schedule
$response = $this->client->call(Client::METHOD_POST, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'resourceType' => 'function',
'resourceId' => ID::unique(),
]);
$this->assertEquals(400, $response['headers']['status-code']);
return $data;
}
/**
* @depends testCreateSchedule
*/
public function testGetSchedule($data): array
{
$id = $data['projectId'] ?? '';
$scheduleId = $data['scheduleId'] ?? '';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/schedules/'.$scheduleId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEquals($scheduleId, $response['body']['$id']);
$this->assertEquals('function', $response['body']['resourceType']);
$this->assertEquals('0 0 * * *', $response['body']['schedule']);
$this->assertArrayHasKey('data', $response['body']);
$this->assertTrue($response['body']['active']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/schedules/'.ID::unique(), array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(404, $response['headers']['status-code']);
return $data;
}
/**
* @depends testCreateSchedule
*/
public function testListSchedules($data): array
{
$id = $data['projectId'] ?? '';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsArray($response['body']['schedules']);
$this->assertGreaterThanOrEqual(1, $response['body']['total']);
$this->assertGreaterThanOrEqual(1, \count($response['body']['schedules']));
// Verify schedule structure
$schedule = $response['body']['schedules'][0];
$this->assertArrayHasKey('$id', $schedule);
$this->assertArrayHasKey('$createdAt', $schedule);
$this->assertArrayHasKey('$updatedAt', $schedule);
$this->assertArrayHasKey('resourceType', $schedule);
$this->assertArrayHasKey('resourceId', $schedule);
$this->assertArrayHasKey('projectId', $schedule);
$this->assertArrayHasKey('schedule', $schedule);
$this->assertArrayHasKey('data', $schedule);
$this->assertArrayHasKey('active', $schedule);
$this->assertArrayHasKey('region', $schedule);
/** Filter by resourceType */
$response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::equal('resourceType', ['function'])->toString()],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThanOrEqual(1, $response['body']['total']);
foreach ($response['body']['schedules'] as $schedule) {
$this->assertEquals('function', $schedule['resourceType']);
}
/** Filter by active status */
$response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::equal('active', [true])->toString()],
]);
$this->assertEquals(200, $response['headers']['status-code']);
foreach ($response['body']['schedules'] as $schedule) {
$this->assertTrue($schedule['active']);
}
/** List with total disabled */
$response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'total' => false,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(0, $response['body']['total']);
$this->assertIsArray($response['body']['schedules']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/projects/'.$id.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::equal('nonexistent', ['value'])->toString()],
]);
$this->assertEquals(400, $response['headers']['status-code']);
return $data;
}
/**
* @depends testCreateSchedule
*/
public function testScheduleProjectIsolation($data): void
{
$scheduleId = $data['scheduleId'] ?? '';
// Create a second project
$team = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'teamId' => ID::unique(),
'name' => 'Isolation Test Team',
]);
$this->assertEquals(201, $team['headers']['status-code']);
$otherProject = $this->client->call(Client::METHOD_POST, '/projects', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'projectId' => ID::unique(),
'name' => 'Isolation Test Project',
'teamId' => $team['body']['$id'],
'region' => System::getEnv('_APP_REGION', 'default'),
]);
$this->assertEquals(201, $otherProject['headers']['status-code']);
$otherProjectId = $otherProject['body']['$id'];
// Try to get the schedule from the other project
$response = $this->client->call(Client::METHOD_GET, '/projects/'.$otherProjectId.'/schedules/'.$scheduleId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(404, $response['headers']['status-code']);
// List should not include schedules from other projects
$response = $this->client->call(Client::METHOD_GET, '/projects/'.$otherProjectId.'/schedules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(200, $response['headers']['status-code']);
$scheduleIds = array_column($response['body']['schedules'], '$id');
$this->assertNotContains($scheduleId, $scheduleIds);
}
}