From 64ed422277bc4580cfb318ab5fc8147d404c4f80 Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Fri, 13 Feb 2026 12:01:21 +0000 Subject: [PATCH] Add schedules API endpoints (GET, XList, Create) --- app/config/scopes/project.php | 6 + app/init/models.php | 3 + docs/references/schedules/create.md | 1 + docs/references/schedules/get.md | 1 + docs/references/schedules/list.md | 1 + src/Appwrite/Platform/Appwrite.php | 2 + .../Schedules/Http/Schedules/Create.php | 96 ++++++++++++++ .../Modules/Schedules/Http/Schedules/Get.php | 76 ++++++++++++ .../Schedules/Http/Schedules/XList.php | 117 ++++++++++++++++++ .../Platform/Modules/Schedules/Module.php | 14 +++ .../Modules/Schedules/Services/Http.php | 20 +++ .../Database/Validator/Queries/Schedules.php | 24 ++++ src/Appwrite/Utopia/Response.php | 4 + .../Utopia/Response/Model/Schedule.php | 95 ++++++++++++++ 14 files changed, 460 insertions(+) create mode 100644 docs/references/schedules/create.md create mode 100644 docs/references/schedules/get.md create mode 100644 docs/references/schedules/list.md create mode 100644 src/Appwrite/Platform/Modules/Schedules/Http/Schedules/Create.php create mode 100644 src/Appwrite/Platform/Modules/Schedules/Http/Schedules/Get.php create mode 100644 src/Appwrite/Platform/Modules/Schedules/Http/Schedules/XList.php create mode 100644 src/Appwrite/Platform/Modules/Schedules/Module.php create mode 100644 src/Appwrite/Platform/Modules/Schedules/Services/Http.php create mode 100644 src/Appwrite/Utopia/Database/Validator/Queries/Schedules.php create mode 100644 src/Appwrite/Utopia/Response/Model/Schedule.php diff --git a/app/config/scopes/project.php b/app/config/scopes/project.php index a6ad98e4e0..0a3feba9e7 100644 --- a/app/config/scopes/project.php +++ b/app/config/scopes/project.php @@ -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', ], diff --git a/app/init/models.php b/app/init/models.php index b935136a63..d432852660 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -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()); diff --git a/docs/references/schedules/create.md b/docs/references/schedules/create.md new file mode 100644 index 0000000000..a10a0db1e7 --- /dev/null +++ b/docs/references/schedules/create.md @@ -0,0 +1 @@ +Create a new schedule for a resource. \ No newline at end of file diff --git a/docs/references/schedules/get.md b/docs/references/schedules/get.md new file mode 100644 index 0000000000..3a78e273e0 --- /dev/null +++ b/docs/references/schedules/get.md @@ -0,0 +1 @@ +Get a schedule by its unique ID. \ No newline at end of file diff --git a/docs/references/schedules/list.md b/docs/references/schedules/list.md new file mode 100644 index 0000000000..fe564386fa --- /dev/null +++ b/docs/references/schedules/list.md @@ -0,0 +1 @@ +Get a list of all the project's schedules. You can use the query params to filter your results. \ No newline at end of file diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php index 9982b0bf1e..1007e8ee45 100644 --- a/src/Appwrite/Platform/Appwrite.php +++ b/src/Appwrite/Platform/Appwrite.php @@ -11,6 +11,7 @@ use Appwrite\Platform\Modules\Functions; use Appwrite\Platform\Modules\Health; use Appwrite\Platform\Modules\Projects; use Appwrite\Platform\Modules\Proxy; +use Appwrite\Platform\Modules\Schedules; use Appwrite\Platform\Modules\Sites; use Appwrite\Platform\Modules\Storage; use Appwrite\Platform\Modules\Tokens; @@ -28,6 +29,7 @@ class Appwrite extends Platform $this->addModule(new Projects\Module()); $this->addModule(new Functions\Module()); $this->addModule(new Health\Module()); + $this->addModule(new Schedules\Module()); $this->addModule(new Sites\Module()); $this->addModule(new Console\Module()); $this->addModule(new Proxy\Module()); diff --git a/src/Appwrite/Platform/Modules/Schedules/Http/Schedules/Create.php b/src/Appwrite/Platform/Modules/Schedules/Http/Schedules/Create.php new file mode 100644 index 0000000000..d3d4372172 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Schedules/Http/Schedules/Create.php @@ -0,0 +1,96 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/schedules') + ->desc('Create schedule') + ->groups(['api', 'schedules']) + ->label('scope', 'schedules.write') + ->label('audits.event', 'schedule.create') + ->label('audits.resource', 'schedule/{response.$id}') + ->label('sdk', new Method( + namespace: 'schedules', + group: 'schedules', + name: 'create', + description: '/docs/references/schedules/create.md', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_CREATED, + model: Response::MODEL_SCHEDULE, + ) + ], + )) + ->param('resourceType', '', new WhiteList([SCHEDULE_RESOURCE_TYPE_FUNCTION, SCHEDULE_RESOURCE_TYPE_EXECUTION, SCHEDULE_RESOURCE_TYPE_MESSAGE], true), 'The resource type for the schedule. Possible values: ' . implode(', ', [SCHEDULE_RESOURCE_TYPE_FUNCTION, SCHEDULE_RESOURCE_TYPE_EXECUTION, SCHEDULE_RESOURCE_TYPE_MESSAGE]) . '.') + ->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) + ->inject('response') + ->inject('project') + ->inject('dbForPlatform') + ->inject('authorization') + ->callback($this->action(...)); + } + + public function action( + string $resourceType, + string $resourceId, + string $schedule, + bool $active, + Response $response, + Document $project, + Database $dbForPlatform, + Authorization $authorization, + ): void { + try { + $doc = $authorization->skip( + fn () => $dbForPlatform->createDocument('schedules', new Document([ + 'region' => $project->getAttribute('region'), + 'resourceType' => $resourceType, + 'resourceId' => $resourceId, + 'resourceInternalId' => '', + 'resourceUpdatedAt' => DateTime::now(), + 'projectId' => $project->getId(), + 'schedule' => $schedule, + 'active' => $active, + ])) + ); + } catch (DuplicateException) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to create schedule. Please try again.'); + } + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($doc, Response::MODEL_SCHEDULE); + } +} diff --git a/src/Appwrite/Platform/Modules/Schedules/Http/Schedules/Get.php b/src/Appwrite/Platform/Modules/Schedules/Http/Schedules/Get.php new file mode 100644 index 0000000000..afc617c063 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Schedules/Http/Schedules/Get.php @@ -0,0 +1,76 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/schedules/:scheduleId') + ->desc('Get schedule') + ->groups(['api', 'schedules']) + ->label('scope', 'schedules.read') + ->label('sdk', new Method( + namespace: 'schedules', + group: 'schedules', + name: 'get', + description: '/docs/references/schedules/get.md', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_SCHEDULE, + ) + ] + )) + ->param('scheduleId', '', new UID(), 'Schedule ID.') + ->inject('response') + ->inject('project') + ->inject('dbForPlatform') + ->inject('authorization') + ->callback($this->action(...)); + } + + public function action( + string $scheduleId, + Response $response, + Document $project, + Database $dbForPlatform, + Authorization $authorization, + ): void { + $schedule = $authorization->skip( + fn () => $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); + } +} diff --git a/src/Appwrite/Platform/Modules/Schedules/Http/Schedules/XList.php b/src/Appwrite/Platform/Modules/Schedules/Http/Schedules/XList.php new file mode 100644 index 0000000000..c140156804 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Schedules/Http/Schedules/XList.php @@ -0,0 +1,117 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/schedules') + ->desc('List schedules') + ->groups(['api', 'schedules']) + ->label('scope', 'schedules.read') + ->label('sdk', new Method( + namespace: 'schedules', + group: 'schedules', + name: 'list', + description: '/docs/references/schedules/list.md', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_SCHEDULE_LIST, + ) + ] + )) + ->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('project') + ->inject('dbForPlatform') + ->inject('authorization') + ->callback($this->action(...)); + } + + public function action( + array $queries, + bool $includeTotal, + Response $response, + Document $project, + Database $dbForPlatform, + Authorization $authorization, + ): void { + try { + $queries = Query::parseQueries($queries); + } catch (QueryException $e) { + throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage()); + } + + // Always scope to the current project + $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 = $authorization->skip( + fn () => $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 = $authorization->skip( + fn () => $dbForPlatform->find('schedules', $queries) + ); + $total = $includeTotal ? $authorization->skip( + fn () => $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); + } +} diff --git a/src/Appwrite/Platform/Modules/Schedules/Module.php b/src/Appwrite/Platform/Modules/Schedules/Module.php new file mode 100644 index 0000000000..39fcfa0175 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Schedules/Module.php @@ -0,0 +1,14 @@ +addService('http', new Http()); + } +} diff --git a/src/Appwrite/Platform/Modules/Schedules/Services/Http.php b/src/Appwrite/Platform/Modules/Schedules/Services/Http.php new file mode 100644 index 0000000000..1ea5baf0ce --- /dev/null +++ b/src/Appwrite/Platform/Modules/Schedules/Services/Http.php @@ -0,0 +1,20 @@ +type = Service::TYPE_HTTP; + + $this->addAction(Get::getName(), new Get()); + $this->addAction(XList::getName(), new XList()); + $this->addAction(Create::getName(), new Create()); + } +} diff --git a/src/Appwrite/Utopia/Database/Validator/Queries/Schedules.php b/src/Appwrite/Utopia/Database/Validator/Queries/Schedules.php new file mode 100644 index 0000000000..6798985c22 --- /dev/null +++ b/src/Appwrite/Utopia/Database/Validator/Queries/Schedules.php @@ -0,0 +1,24 @@ +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' => 'The date the associated resource was last updated 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('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; + } +}