mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge pull request #11331 from appwrite/feat-schedules-endpoints
Add schedules API endpoints
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -91,6 +91,8 @@ $admins = [
|
||||
'subscribers.read',
|
||||
'tokens.read',
|
||||
'tokens.write',
|
||||
'schedules.read',
|
||||
'schedules.write',
|
||||
];
|
||||
|
||||
return [
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user