Mojolicious-Plugin-Fondation-OpenAPI

 view release on metacpan or  search on metacpan

lib/Mojolicious/Plugin/Fondation/OpenAPI/Command/openapi.pm  view on Meta::CPAN

        unless ($schema->await($rs_gp->find({ group_id => $admin->id, perm_id => $perm->id }))) {
            eval { $schema->await($rs_gp->create({ group_id => $admin->id, perm_id => $perm->id })) };
            if ($@) {
                warn "[sync-permissions] Failed to assign '$name' to admin: $@\n";
            } else {
                $assigned++;
            }
        }
    }
    warn "[sync-permissions] Assigned $assigned permission(s) to 'admin' group.\n";
}

# ---------------------------------------------------------------------------
# Build client-side validators.js from the OpenAPI spec
# ---------------------------------------------------------------------------

sub _build_validators_js ($self, $spec) {
    my $schemas = $spec->{components}{schemas};
    my $schemas_js = '';

    for my $name (sort keys %$schemas) {
        my $schema = $schemas->{$name};
        my $props  = $schema->{properties} // {};

        $schemas_js .= "FondationSchemas['$name'] = {\n";
        $schemas_js .= "  properties: {\n";

        for my $prop (sort keys %$props) {
            my $def = $props->{$prop};
            my @rules;

            push @rules, "required: true"
                if grep { $_ eq $prop } @{ $schema->{required} // [] };
            push @rules, "type: '" . $def->{type} . "'"
                if $def->{type};
            push @rules, "minLength: " . $def->{minLength}
                if $def->{minLength};
            push @rules, "maxLength: " . $def->{maxLength}
                if $def->{maxLength};
            push @rules, "format: '" . $def->{format} . "'"
                if $def->{format};
            push @rules, "nullable: true"
                if $def->{nullable};
            push @rules, "readOnly: true"
                if $def->{readOnly};
            push @rules, "writeOnly: true"
                if $def->{writeOnly};

            $schemas_js .= "    '$prop': { " . join(', ', @rules) . " },\n";
        }

        $schemas_js .= "  }\n";
        $schemas_js .= "};\n\n";
    }

    my $validators = <<'VALIDATORS';
var FondationSchemas = {};
SCHEMAS_PLACEHOLDER

window.FondationValidators = {
    validate: function(schemaName, data) {
        var schema = FondationSchemas[schemaName];
        if (!schema) return { valid: false, errors: ['Schema not found: ' + schemaName] };

        var errors = [];

        for (var prop in schema.properties) {
            var rules = schema.properties[prop];
            var val   = data[prop];

            // Skip readOnly fields (server-managed, not in forms)
            if (rules.readOnly) continue;

            // Required check (skip readOnly -- server-managed)
            if (rules.required && !rules.readOnly) {
                if (val === undefined || val === null || val === '') {
                    errors.push(prop + ' is required');
                    continue;
                }
            }

            // Skip further checks if value is empty and not required
            if (val === undefined || val === null || val === '') {
                if (rules.nullable) continue;
                continue;
            }

            // Type check
            if (rules.type === 'integer') {
                var n = Number(val);
                if (isNaN(n) || !Number.isInteger(n)) {
                    errors.push(prop + ' must be an integer');
                    continue;
                }
            }

            // Format check
            if (rules.format === 'email') {
                if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) {
                    errors.push(prop + ' must be a valid email');
                }
            }

            // Min length
            if (rules.minLength && typeof val === 'string' && val.length < rules.minLength) {
                errors.push(prop + ' must be at least ' + rules.minLength + ' characters');
            }

            // Max length
            if (rules.maxLength && typeof val === 'string' && val.length > rules.maxLength) {
                errors.push(prop + ' must be at most ' + rules.maxLength + ' characters');
            }

            // Enum check
            if (rules.enum) {
                var match = false;
                for (var i = 0; i < rules.enum.length; i++) {
                    if (rules.type === 'integer') {
                        if (Number(val) === rules.enum[i]) { match = true; break; }
                    } else {
                        if (val === String(rules.enum[i])) { match = true; break; }

lib/Mojolicious/Plugin/Fondation/OpenAPI/Command/openapi.pm  view on Meta::CPAN


=item * Added back into C<create> and C<update> projection properties

=back

This means GET responses never contain writeOnly fields and the
OpenAPI validator does not expect them.

=head2 Config override

Any column property can be overridden via the plugin configuration
without modifying DBIx classes:

  'Fondation::OpenAPI' => {
      backend => 'main',
      schemas => {
          User => {
              columns => {
                  name => {
                      maxLength => 100,       # override DBIx size
                  },
                  password => {
                      writeOnly => 1,
                      create    => { required => 1 },
                      update    => { required => 0 },
                  },
              },
          },
      },
  },

Config keys follow the same flat + contextual structure as
C<extra-E<gt>{openapi}> and take the highest priority in the cascade:

  1. Structure DBIx (implicit)
  2. extra->{openapi} flat keys (Result class)
  3. Config flat keys (myapp.conf)
  4. extra->{openapi} contextual (Result class)
  5. Config contextual (myapp.conf)

=head1 SUBCOMMANDS

=head2 generate

  myapp.pl openapi generate
  myapp.pl openapi generate -y
  myapp.pl openapi generate --output custom.json

Iterates all DBIx sources (monikers), builds the API Base for each,
applies contextual projections, and writes two files:

=over

=item C<share/openapi.json>

OpenAPI 3.0.3 specification with schemas and CRUD paths. Loaded at
runtime by L<Mojolicious::Plugin::OpenAPI> for request validation.

=item C<public/js/validators.js>

Client-side validation (C<FondationValidators.validate()>) consumed
by L<Fondation::Asset> bundles.

=back

Options:

  --output FILE   Output path relative to $app->home (default: share/openapi.json)
  -y              Overwrite without confirmation prompt

=head1 CRUD PATHS

Each source generates five endpoints with C<x-mojo-to> routing and
automatic C<x-auth> permission annotations:

  GET    /{moniker}       -> {Moniker}#list    x-auth: {moniker_lc}_list
  POST   /{moniker}       -> {Moniker}#create  x-auth: {moniker_lc}_create
  GET    /{moniker}/{id}  -> {Moniker}#read    x-auth: {moniker_lc}_read
  PUT    /{moniker}/{id}  -> {Moniker}#update  x-auth: {moniker_lc}_update
  DELETE /{moniker}/{id}  -> {Moniker}#delete  x-auth: {moniker_lc}_delete

The C<x-auth> default can be overridden via the plugin config
(C<schemas.{Source}.x_auth.{operation}>). See L<Mojolicious::Plugin::Fondation::OpenAPI>
for details. Enforcement is handled at runtime by
L<Mojolicious::Plugin::Fondation::OpenAPI::Security>.

=head1 SEE ALSO

L<Mojolicious::Plugin::Fondation::OpenAPI>,
L<Fondation::Model::DBIx::Async>,
L<Mojolicious::Plugin::OpenAPI>

=head1 AUTHOR

Daniel Brosseau <dab@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Daniel Brosseau.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut



( run in 0.925 second using v1.01-cache-2.11-cpan-f4a522933cf )