NAME

Concierge::Base - Records-store base class for Concierge component modules

VERSION

v0.7.0

SYNOPSIS

package Concierge::Organizations;
use parent 'Concierge::Base';

sub new ($class, $config_file) {
    my $self = bless {}, $class;
    $self->{config_file} = $config_file;
    return $self;
}

sub setup ($self, $config) {
    # Initialize storage from desk config block
    # $config is the hashref for your component in concierge.conf
    return { success => 1, message => 'Organizations ready' };
}

sub add_record ($self, $id, $data) {
    # Persist a new organization record
    # ...
    return { success => 1, message => "Organization '$id' added", id => $id };
}

# ... implement remaining methods ...

1;

DESCRIPTION

Concierge::Base is an abstract base class for records-store components that integrate with Concierge desks. It documents the method contract that Concierge expects from any additional component -- such as Concierge::Organizations, Concierge::Assets, or similar -- and provides stub implementations that die informatively if a subclass omits a required method.

Concierge::Base does not depend on any of the identity core modules (Concierge::Auth, Concierge::Sessions, Concierge::Users) and does not need to be used alongside them. It is purely a contract-documentation and safety-net class.

Return Convention

All methods in a conforming subclass should return a hashref:

# Success, no payload beyond confirmation
{ success => 1, message => 'Record added' }

# Success with payload
{ success => 1, message => 'Record found', record => \%data }

# Failure
{ success => 0, message => 'Record not found' }

The success key is always 0 or 1. The message key is always a human-readable string. Additional payload keys (record, records, ids, etc.) may be included as appropriate to the method.

Concierge itself follows this convention throughout; adopting it in additional components makes error handling uniform across the entire desk.

METHODS

new

my $component = Concierge::Organizations->new($config_file);

Constructor. Receives the path to the component's configuration file (or undef if the component does not use a separate file). The subclass is responsible for initializing any storage handles or caches it needs.

Subclasses must override this method.

setup

my $result = $component->setup($config);

One-time setup called during desk initialization (e.g., from Concierge::Setup). $config is a hashref containing the component's block from the desk configuration -- whatever key/value pairs your component needs (storage path, backend type, field schema, etc.).

Returns a { success = 1|0, message => '...' }> hashref.

Subclasses must override this method.

add_record

my $result = $component->add_record($id, \%data);

Creates a new record identified by $id with the fields in %data.

Returns { success = 1|0, message => '...', id => $id }> on success, or { success = 0, message => '...' }> on failure (e.g., duplicate ID).

Subclasses must override this method.

remove_record

my $result = $component->remove_record($id);

Deletes the record identified by $id.

Returns { success = 1|0, message => '...' }>.

Subclasses must override this method.

get_record

# All fields
my $result = $component->get_record($id);

# Selected fields only
my $result = $component->get_record($id, qw(name status));

Retrieves the record identified by $id. If @fields is supplied, returns only those fields; otherwise returns all fields.

Returns { success = 1, message => '...', record => \%data }> on success, or { success = 0, message => '...' }> if the record is not found.

Subclasses must override this method.

update_record

my $result = $component->update_record($id, \%updates);

Applies %updates to the existing record identified by $id. Fields not present in %updates are left unchanged.

Returns { success = 1|0, message => '...' }>.

Subclasses must override this method.

list_records

# All records
my $result = $component->list_records();

# With filter string and options
my $result = $component->list_records('active', { include_data => 1 });

Enumerates records. $filter is an optional string whose interpretation is left to the subclass (e.g., a status value or SQL WHERE fragment). %opts is an optional hashref for pagination, field selection, or other backend-specific options.

Returns { success = 1, ids => \@ids, count => $n }> at minimum, with optional records = \%data> when include_data is requested.

Subclasses must override this method.

INTEGRATING WITH CONCIERGE

Error Return Convention

Concierge itself and all its component modules return { success = 1|0, message => '...' }> from every method. Conforming to this convention in your component lets application code use a single, uniform error-handling idiom:

my $result = $concierge->some_operation(...);
unless ($result->{success}) {
    # handle $result->{message}
}

Desk Config Block

During desk setup and at open_desk() time, Concierge reads concierge.conf (a JSON file in the desk directory). To integrate a new component, add a key for it in that file:

{
    "sessions_dir": "/path/to/desk",
    "users_config_file": "/path/to/desk/users.conf",
    "auth_file": "/path/to/desk/auth.json",
    "organizations_config": {
        "backend": "sqlite",
        "db_file": "/path/to/desk/orgs.db"
    }
}

Your component's setup() method receives the value of that key as $config:

sub setup ($self, $config) {
    my $db = $config->{db_file};
    # initialize storage ...
}

open_desk Hook

To load your component automatically when a desk is opened, the recommended pattern is to subclass or monkey-patch Concierge::open_desk(), or to provide a thin wrapper around it in your application:

my $result = Concierge->open_desk($desk_dir);
my $c = $result->{concierge};

# Load your component
my $orgs = Concierge::Organizations->new(
    $desk_config->{organizations_config}{config_file}
);
$orgs->setup($desk_config->{organizations_config});
$c->{organizations} = $orgs;

Alternatively, a future version of Concierge may provide a hook point for additional components. See the EXTENSIBILITY section in Concierge for the current recommended approach.

SUBCLASSING

A minimal subclass skeleton:

package Concierge::Organizations;
use v5.36;
use parent 'Concierge::Base';
use Carp qw<croak>;

our $VERSION = 'v0.1.0';

sub new ($class, $config_file=undef) {
    bless { config_file => $config_file, records => {} }, $class;
}

sub setup ($self, $config) {
    # $config comes from the desk's concierge.conf
    $self->{storage_path} = $config->{storage_path}
        or return { success => 0, message => 'storage_path required' };
    # ... initialize backend ...
    return { success => 1, message => 'Organizations initialized' };
}

sub add_record ($self, $id, $data) {
    return { success => 0, message => 'id is required' }
        unless defined $id && length $id;
    return { success => 0, message => "Record '$id' already exists" }
        if exists $self->{records}{$id};
    $self->{records}{$id} = $data;
    return { success => 1, message => "Organization '$id' added", id => $id };
}

sub remove_record ($self, $id) {
    return { success => 0, message => "Record '$id' not found" }
        unless exists $self->{records}{$id};
    delete $self->{records}{$id};
    return { success => 1, message => "Organization '$id' removed" };
}

sub get_record ($self, $id, @fields) {
    return { success => 0, message => "Record '$id' not found" }
        unless exists $self->{records}{$id};
    my $data = $self->{records}{$id};
    if (@fields) {
        my %selected = map { $_ => $data->{$_} }
                       grep { exists $data->{$_} } @fields;
        return { success => 1, record => \%selected };
    }
    return { success => 1, record => $data };
}

sub update_record ($self, $id, $updates) {
    return { success => 0, message => "Record '$id' not found" }
        unless exists $self->{records}{$id};
    $self->{records}{$id} = { %{$self->{records}{$id}}, %$updates };
    return { success => 1, message => "Organization '$id' updated" };
}

sub list_records ($self, $filter='', $opts={}) {
    my @ids = sort keys %{$self->{records}};
    return { success => 1, ids => \@ids, count => scalar @ids }
        unless $opts->{include_data};
    my %records = map { $_ => $self->{records}{$_} } @ids;
    return { success => 1, ids => \@ids, records => \%records, count => scalar @ids };
}

1;

SEE ALSO

Concierge -- main orchestrator; see its EXTENSIBILITY section for the component substitution and addition pattern.

Concierge::Setup -- desk creation and configuration.

Concierge::Users -- the identity core records-store component, which provides a production example of a records-store component integrated with a Concierge desk.

AUTHOR

Bruce Van Allen <bva@cruzio.com>

LICENSE

This module is free software; you can redistribute it and/or modify it under the terms of the Artistic License 2.0.